2 // PBGitCommitController.m
5 // Created by Pieter de Bie on 19-09-08.
6 // Copyright 2008 __MyCompanyName__. All rights reserved.
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;
23 @implementation PBGitCommitController
25 @synthesize files, status, busy, amend;
29 self.files = [NSMutableArray array];
33 [commitMessageView setTypingAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Monaco" size:12.0] forKey:NSFontAttributeName]];
35 [unstagedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasUnstagedChanges == 1"]];
36 [cachedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasStagedChanges == 1"]];
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]]];
46 [webController closeView];
50 - (void) setAmend:(BOOL)newAmend
52 if (newAmend == amend)
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;
71 - (NSArray *) linesFromNotification:(NSNotification *)notification
73 NSDictionary *userInfo = [notification userInfo];
74 NSData *data = [userInfo valueForKey:NSFileHandleNotificationDataItem];
78 NSString* string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
82 // Strip trailing newline
83 if ([string hasSuffix:@"\n"])
84 string = [string substringToIndex:[string length]-1];
86 NSArray *lines = [string componentsSeparatedByString:@"\0"];
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";
101 - (void) refresh:(id) sender
103 if (![repository workingDirectory])
106 self.status = @"Refreshing index…";
108 // If self.busy reaches 0, all tasks have finished
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];
123 [handle readToEndOfFileInBackgroundAndNotify];
126 handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]];
127 [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
129 [handle readToEndOfFileInBackgroundAndNotify];
132 handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]];
133 [nc addObserver:self selector:@selector(readCachedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
135 [handle readToEndOfFileInBackgroundAndNotify];
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"];
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];
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)
171 [dictionary setObject:fileStatus forKey:path];
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
184 for (NSString *line in lines) {
187 fileStatus = [line componentsSeparatedByString:@" "];
192 [dictionary setObject:fileStatus forKey:line];
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
205 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
206 NSString *sha = [fileStatus objectAtIndex:2];
209 file.hasStagedChanges = YES;
210 file.commitBlobSHA = sha;
211 file.commitBlobMode = mode;
213 file.hasUnstagedChanges = YES;
215 // Untracked file, set status to NEW, only unstaged changes
216 file.hasStagedChanges = NO;
217 file.hasUnstagedChanges = YES;
220 [dictionary removeObjectForKey:file.path];
221 } else { // Object not found, let's remove it from the changes
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;
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"])
241 file.status = MODIFIED;
244 file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
245 file.commitBlobSHA = [fileStatus objectAtIndex:2];
248 file.hasStagedChanges = staged;
249 file.hasUnstagedChanges = !staged;
251 [files addObject: file];
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
274 self.status = [@"Commit failed: " stringByAppendingString:reason];
275 [[NSAlert alertWithMessageText:@"Commit failed"
279 informativeTextWithFormat:reason] runModal];
283 - (IBAction) commit:(id) sender
285 if ([[cachedFilesController arrangedObjects] count] == 0) {
286 [[NSAlert alertWithMessageText:@"No changes to commit"
290 informativeTextWithFormat:@"You must first stage some changes before committing"] runModal];
294 NSString *commitMessage = [commitMessageView string];
295 if ([commitMessage length] < 3) {
296 [[NSAlert alertWithMessageText:@"Commitmessage missing"
300 informativeTextWithFormat:@"Please enter a commit message before committing"] runModal];
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;
312 commitSubject = [commitMessage substringToIndex:newLine.location];
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];
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"];
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];
337 NSString *commit = [repository outputForArguments:arguments
338 inputString:commitMessage
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]
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]];
358 [webController setStateMessage:[NSString stringWithFormat:@"Successfully created commit %@", commit]];
360 repository.hasChanged = YES;
362 [commitMessageView setString:@""];
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];
382 [array addObject:@"--cached"];
384 [array addObject:@"--reverse"];
387 NSString *error = [repository outputForArguments:array
391 // FIXME: show this error, rather than just logging it
393 NSLog(@"Error: %@", error);
395 // TODO: We should do this smarter by checking if the file diff is empty, which is faster.