NSSTring_RegEx: Add support for regular expressions to NSString
[GitX.git] / PBGitCommitController.m
blob8a276d3cfb246732a6fd96395ea3ae9844f22f6a
1 //
2 //  PBGitCommitController.m
3 //  GitX
4 //
5 //  Created by Pieter de Bie on 19-09-08.
6 //  Copyright 2008 __MyCompanyName__. All rights reserved.
7 //
9 #import "PBGitCommitController.h"
10 #import "NSFileHandleExt.h"
11 #import "PBChangedFile.h"
12 #import "PBWebChangesController.h"
15 @interface PBGitCommitController (PrivateMethods)
16 - (NSArray *) linesFromNotification:(NSNotification *)notification;
17 - (void) doneProcessingIndex;
18 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines;
19 - (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked;
20 - (void)processHunk:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse;
21 @end
23 @implementation PBGitCommitController
25 @synthesize files, status, busy, amend;
27 - (void)awakeFromNib
29         self.files = [NSMutableArray array];
30         [super awakeFromNib];
31         [self refresh:self];
33         [commitMessageView setTypingAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Monaco" size:12.0] forKey:NSFontAttributeName]];
34         
35         [unstagedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasUnstagedChanges == 1"]];
36         [cachedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasStagedChanges == 1"]];
37         
38         [unstagedFilesController setSortDescriptors:[NSArray arrayWithObjects:
39                 [[NSSortDescriptor alloc] initWithKey:@"status" ascending:false],
40                 [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true], nil]];
41         [cachedFilesController setSortDescriptors:[NSArray arrayWithObject:
42                 [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true]]];
44 - (void) removeView
46         [webController closeView];
47         [super finalize];
50 - (void) setAmend:(BOOL)newAmend
52         if (newAmend == amend)
53                 return;
54         amend = newAmend;
56         // Replace commit message with the old one if it's less than 3 characters long.
57         // This is just a random number.
58         if (amend && [[commitMessageView string] length] <= 3) {
59                 NSString *message = [repository outputForCommand:@"cat-file commit HEAD"];
60                 NSRange r = [message rangeOfString:@"\n\n"];
61                 if (r.location != NSNotFound)
62                         message = [message substringFromIndex:r.location + 2];
64                 commitMessageView.string = message;
65         }
68         [self refresh:self];
71 - (NSArray *) linesFromNotification:(NSNotification *)notification
73         NSDictionary *userInfo = [notification userInfo];
74         NSData *data = [userInfo valueForKey:NSFileHandleNotificationDataItem];
75         if (!data)
76                 return NULL;
77         
78         NSString* string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
79         if (!string)
80                 return NULL;
81         
82         // Strip trailing newline
83         if ([string hasSuffix:@"\n"])
84                 string = [string substringToIndex:[string length]-1];
85         
86         NSArray *lines = [string componentsSeparatedByString:@"\0"];
87         return lines;
90 - (NSString *) parentTree
92         NSString *parent = amend ? @"HEAD^" : @"HEAD";
94         if (![repository parseReference:parent])
95                 // We don't have a head ref. Return the empty tree.
96                 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
98         return parent;
101 - (void) refresh:(id) sender
103         if (![repository workingDirectory])
104                 return;
106         self.status = @"Refreshing index…";
108         // If self.busy reaches 0, all tasks have finished
109         self.busy = 0;
111         // Refresh the index, necessary for the next methods (that's why it's blocking)
112         // FIXME: Make this non-blocking. This call can be expensive in large repositories
113         [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]];
115         NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 
116         [nc removeObserver:self]; 
118         // Other files (not tracked, not ignored)
119         NSArray *arguments = [NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil];
120         NSFileHandle *handle = [repository handleInWorkDirForArguments:arguments];
121         [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; 
122         self.busy++;
123         [handle readToEndOfFileInBackgroundAndNotify];
124         
125         // Unstaged files
126         handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]];
127         [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; 
128         self.busy++;
129         [handle readToEndOfFileInBackgroundAndNotify];
131         // Staged files
132         handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]];
133         [nc addObserver:self selector:@selector(readCachedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; 
134         self.busy++;
135         [handle readToEndOfFileInBackgroundAndNotify];
138 - (void) updateView
140         [self refresh:nil];
143 // This method is called for each of the three processes from above.
144 // If all three are finished (self.busy == 0), then we can delete
145 // all files previously marked as deletable
146 - (void) doneProcessingIndex
148         [self willChangeValueForKey:@"files"];
149         if (!--self.busy) {
150                 self.status = @"Ready";
151                 for (PBChangedFile *file in files) {
152                         if (!file.hasStagedChanges && !file.hasUnstagedChanges) {
153                                 NSLog(@"Deleting file: %@", [file path]);
154                                 [files removeObject:file];
155                         }
156                 }
157         }
158         [self didChangeValueForKey:@"files"];
161 - (void) readOtherFiles:(NSNotification *)notification;
163         [unstagedFilesController setAutomaticallyRearrangesObjects:NO];
164         NSArray *lines = [self linesFromNotification:notification];
165         NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:[lines count]];
166         // We fake this files status as good as possible.
167         NSArray *fileStatus = [NSArray arrayWithObjects:@":000000", @"100644", @"0000000000000000000000000000000000000000", @"0000000000000000000000000000000000000000", @"A", nil];
168         for (NSString *path in lines) {
169                 if ([path length] == 0)
170                         continue;
171                 [dictionary setObject:fileStatus forKey:path];
172         }
173         [self addFilesFromDictionary:dictionary staged:NO tracked:NO];
174         [self doneProcessingIndex];
177 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines
179         NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2];
181         // Fill the dictionary with the new information
182         NSArray *fileStatus;
183         BOOL even = FALSE;
184         for (NSString *line in lines) {
185                 if (!even) {
186                         even = TRUE;
187                         fileStatus = [line componentsSeparatedByString:@" "];
188                         continue;
189                 }
191                 even = FALSE;
192                 [dictionary setObject:fileStatus forKey:line];
193         }
194         return dictionary;
197 - (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked
199         // Iterate over all existing files
200         for (PBChangedFile *file in files) {
201                 NSArray *fileStatus = [dictionary objectForKey:file.path];
202                 // Object found, this is still a cached / uncached thing
203                 if (fileStatus) {
204                         if (tracked) {
205                                 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
206                                 NSString *sha = [fileStatus objectAtIndex:2];
208                                 if (staged) {
209                                         file.hasStagedChanges = YES;
210                                         file.commitBlobSHA = sha;
211                                         file.commitBlobMode = mode;
212                                 } else
213                                         file.hasUnstagedChanges = YES;
214                         } else {
215                                 // Untracked file, set status to NEW, only unstaged changes
216                                 file.hasStagedChanges = NO;
217                                 file.hasUnstagedChanges = YES;
218                                 file.status = NEW;
219                         }
220                         [dictionary removeObjectForKey:file.path];
221                 } else { // Object not found, let's remove it from the changes
222                         if (staged)
223                                 file.hasStagedChanges = NO;
224                         else if (tracked && file.status != NEW) // Only remove it if it's not an untracked file. We handle that with the other thing
225                                 file.hasUnstagedChanges = NO;
226                         else if (!tracked && file.status == NEW)
227                                 file.hasUnstagedChanges = NO;
228                 }
229         }
231         // Do new files
232         for (NSString *path in [dictionary allKeys]) {
233                 NSArray *fileStatus = [dictionary objectForKey:path];
235                 PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path];
236                 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
237                         file.status = DELETED;
238                 else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"])
239                         file.status = NEW;
240                 else
241                         file.status = MODIFIED;
243                 if (staged) {
244                         file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
245                         file.commitBlobSHA = [fileStatus objectAtIndex:2];
246                 }
248                 file.hasStagedChanges = staged;
249                 file.hasUnstagedChanges = !staged;
251                 [files addObject: file];
252         }
255 - (void) readUnstagedFiles:(NSNotification *)notification
257         NSArray *lines = [self linesFromNotification:notification];
258         NSMutableDictionary *dic = [self dictionaryForLines:lines];
259         [self addFilesFromDictionary:dic staged:NO tracked:YES];
260         [self doneProcessingIndex];
263 - (void) readCachedFiles:(NSNotification *)notification
265         NSArray *lines = [self linesFromNotification:notification];
266         NSMutableDictionary *dic = [self dictionaryForLines:lines];
267         [self addFilesFromDictionary:dic staged:YES tracked:YES];
268         [self doneProcessingIndex];
271 - (void) commitFailedBecause:(NSString *)reason
273         self.busy--;
274         self.status = [@"Commit failed: " stringByAppendingString:reason];
275         [[NSAlert alertWithMessageText:@"Commit failed"
276                                          defaultButton:nil
277                                    alternateButton:nil
278                                            otherButton:nil
279                  informativeTextWithFormat:reason] runModal];
280         return;
283 - (IBAction) commit:(id) sender
285         if ([[cachedFilesController arrangedObjects] count] == 0) {
286                 [[NSAlert alertWithMessageText:@"No changes to commit"
287                                                  defaultButton:nil
288                                            alternateButton:nil
289                                                    otherButton:nil
290                          informativeTextWithFormat:@"You must first stage some changes before committing"] runModal];
291                 return;
292         }               
293         
294         NSString *commitMessage = [commitMessageView string];
295         if ([commitMessage length] < 3) {
296                 [[NSAlert alertWithMessageText:@"Commitmessage missing"
297                                                  defaultButton:nil
298                                            alternateButton:nil
299                                                    otherButton:nil
300                          informativeTextWithFormat:@"Please enter a commit message before committing"] runModal];
301                 return;
302         }
304         [cachedFilesController setSelectionIndexes:[NSIndexSet indexSet]];
305         [unstagedFilesController setSelectionIndexes:[NSIndexSet indexSet]];
307         NSString *commitSubject;
308         NSRange newLine = [commitMessage rangeOfString:@"\n"];
309         if (newLine.location == NSNotFound)
310                 commitSubject = commitMessage;
311         else
312                 commitSubject = [commitMessage substringToIndex:newLine.location];
313         
314         commitSubject = [@"commit: " stringByAppendingString:commitSubject];
316         NSString *commitMessageFile;
317         commitMessageFile = [repository.fileURL.path
318                                                  stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
320         [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil];
322         self.busy++;
323         self.status = @"Creating tree..";
324         NSString *tree = [repository outputForCommand:@"write-tree"];
325         if ([tree length] != 40)
326                 return [self commitFailedBecause:@"Could not create a tree"];
328         int ret;
330         NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil];
331         NSString *parent = amend ? @"HEAD^" : @"HEAD";
332         if ([repository parseReference:parent]) {
333                 [arguments addObject:@"-p"];
334                 [arguments addObject:parent];
335         }
337         NSString *commit = [repository outputForArguments:arguments
338                                                                                   inputString:commitMessage
339                                                                                          retValue: &ret];
341         if (ret || [commit length] != 40)
342                 return [self commitFailedBecause:@"Could not create a commit object"];
344         if (![repository executeHook:@"pre-commit" output:nil])
345                 return [self commitFailedBecause:@"Pre-commit hook failed"];
347         if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil])
348     return [self commitFailedBecause:@"Commit-msg hook failed"];
350         [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
351                                                   retValue: &ret];
352         if (ret)
353                 return [self commitFailedBecause:@"Could not update HEAD"];
355         if (![repository executeHook:@"post-commit" output:nil])
356                 [webController setStateMessage:[NSString stringWithFormat:@"Post-commit hook failed, however, successfully created commit %@", commit]];
357         else
358                 [webController setStateMessage:[NSString stringWithFormat:@"Successfully created commit %@", commit]];
360         repository.hasChanged = YES;
361         self.busy--;
362         [commitMessageView setString:@""];
363         amend = NO;
364         [self refresh:self];
365         self.amend = NO;
368 - (void) stageHunk:(NSString *)hunk reverse:(BOOL)reverse
370         [self processHunk:hunk stage:TRUE reverse:reverse];
373 - (void)discardHunk:(NSString *)hunk
375         [self processHunk:hunk stage:FALSE reverse:TRUE];
378 - (void)processHunk:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse
380         NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", nil];
381         if (stage)
382                 [array addObject:@"--cached"];
383         if (reverse)
384                 [array addObject:@"--reverse"];
386         int ret = 1;
387         NSString *error = [repository outputForArguments:array
388                                                                                  inputString:hunk
389                                                                                         retValue:&ret];
391         // FIXME: show this error, rather than just logging it
392         if (ret)
393                 NSLog(@"Error: %@", error);
395         // TODO: We should do this smarter by checking if the file diff is empty, which is faster.
396         [self refresh:self]; 
399 @end