GitX 0.6.1
[GitX.git] / PBGitCommitController.m
blob22e995e9e7163c279db10ddd910dbd8dfe3a9683
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"
13 @implementation PBGitCommitController
15 @synthesize files, status, busy, amend;
17 - (void)awakeFromNib
19         self.files = [NSMutableArray array];
20         [super awakeFromNib];
21         [self refresh:self];
23         [commitMessageView setTypingAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Monaco" size:12.0] forKey:NSFontAttributeName]];
24         
25         [unstagedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasUnstagedChanges == 1"]];
26         [cachedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasCachedChanges == 1"]];
27         
28         [unstagedFilesController setSortDescriptors:[NSArray arrayWithObjects:
29                 [[NSSortDescriptor alloc] initWithKey:@"status" ascending:false],
30                 [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true], nil]];
31         [cachedFilesController setSortDescriptors:[NSArray arrayWithObject:
32                 [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true]]];
34 - (void) removeView
36         [webController closeView];
37         [super finalize];
40 - (void) setAmend:(BOOL)newAmend
42         if (newAmend == amend)
43                 return;
44         amend = newAmend;
46         if (amend && [[commitMessageView string] length] <= 3)
47                 commitMessageView.string = [repository outputForCommand:@"log -1 --pretty=format:%s%n%n%b HEAD"];
49         [self refresh:self];
52 - (NSArray *) linesFromNotification:(NSNotification *)notification
54         NSDictionary *userInfo = [notification userInfo];
55         NSData *data = [userInfo valueForKey:NSFileHandleNotificationDataItem];
56         if (!data)
57                 return NULL;
58         
59         NSString* string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
60         if (!string)
61                 return NULL;
62         
63         // Strip trailing newline
64         if ([string hasSuffix:@"\n"])
65                 string = [string substringToIndex:[string length]-1];
66         
67         NSArray *lines = [string componentsSeparatedByString:@"\0"];
68         return lines;
71 - (NSString *) parentTree
73         NSString *parent = amend ? @"HEAD^" : @"HEAD";
75         if (![repository parseReference:parent])
76                 // We don't have a head ref. Return the empty tree.
77                 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
79         return parent;
82 - (void) refresh:(id) sender
84         for (PBChangedFile *file in files)
85                 file.shouldBeDeleted = YES;
87         self.status = @"Refreshing index…";
88         self.busy++;
90         [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]];
91         self.busy--;
93         NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 
94         [nc removeObserver:self]; 
96         // Other files
97         NSArray *arguments = [NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil];
98         NSFileHandle *handle = [repository handleInWorkDirForArguments:arguments];
99         [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; 
100         self.busy++;
101         [handle readToEndOfFileInBackgroundAndNotify];
102         
103         // Unstaged files
104         handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]];
105         [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; 
106         self.busy++;
107         [handle readToEndOfFileInBackgroundAndNotify];
109         // Cached files
110         handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]];
111         [nc addObserver:self selector:@selector(readCachedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; 
112         self.busy++;
113         [handle readToEndOfFileInBackgroundAndNotify];
116 - (void) updateView
118         [self refresh:nil];
121 - (void) doneProcessingIndex
123         [self willChangeValueForKey:@"files"];
124         if (!--self.busy) {
125                 self.status = @"Ready";
126                 NSArray *filesToBeDeleted = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"shouldBeDeleted == 1"]];
127                 for (PBChangedFile *file in filesToBeDeleted) {
128                                 NSLog(@"Deleting file: %@", [file path]);
129                                 [files removeObject:file];
130                 }
131         }
132         [self didChangeValueForKey:@"files"];
135 - (void) readOtherFiles:(NSNotification *)notification;
137         NSArray *lines = [self linesFromNotification:notification];
138         for (NSString *line in lines) {
139                 if ([line length] == 0)
140                         continue;
142                 BOOL added = NO;
143                 // Check if file is already in our index
144                 for (PBChangedFile *file in files) {
145                         if ([[file path] isEqualToString:line]) {
146                                 file.shouldBeDeleted = NO;
147                                 added = YES;
148                                 file.status = NEW;
149                                 file.hasCachedChanges = NO;
150                                 file.hasUnstagedChanges = YES;
151                                 continue;
152                         }
153                 }
155                 if (added)
156                         continue;
158                 // File does not exist yet, so add it
159                 PBChangedFile *file =[[PBChangedFile alloc] initWithPath:line];
160                 file.status = NEW;
161                 file.hasCachedChanges = NO;
162                 file.hasUnstagedChanges = YES;
163                 [files addObject: file];
164         }
166         [self doneProcessingIndex];
169 - (void) addFilesFromLines:(NSArray *)lines cached:(BOOL) cached
171         NSArray *fileStatus;
172         int even = 0;
173         for (NSString *line in lines) {
174                 if (!even) {
175                         even = 1;
176                         fileStatus = [line componentsSeparatedByString:@" "];
177                         continue;
178                 }
179                 even = 0;
181                 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
182                 NSString *sha = [fileStatus objectAtIndex:2];
184                 BOOL isNew = YES;
185                 // If the file is already added, we shouldn't add it again
186                 // but rather update it to incorporate our changes
187                 for (PBChangedFile *file in files) {
188                         if ([file.path isEqualToString:line]) {
189                                 file.shouldBeDeleted = NO;
190                                 if (cached) {
191                                         file.commitBlobSHA = sha;
192                                         file.commitBlobMode = mode;
193                                         file.hasCachedChanges = YES;
194                                 }
195                                 else
196                                         file.hasUnstagedChanges = YES;
198                                 isNew = NO;
199                                 break;
200                         }
201                 }
203                 if (!isNew)
204                         continue;
206                 PBChangedFile *file = [[PBChangedFile alloc] initWithPath:line];
207                 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
208                         file.status = DELETED;
209                 else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"])
210                         file.status = NEW;
211                 else
212                         file.status = MODIFIED;
214                 file.commitBlobSHA = sha;
215                 file.commitBlobMode = mode;
217                 file.hasCachedChanges = cached;
218                 file.hasUnstagedChanges = !cached;
220                 [files addObject: file];
221         }
225 - (void) readUnstagedFiles:(NSNotification *)notification
227         NSArray *lines = [self linesFromNotification:notification];
228         [self addFilesFromLines:lines cached:NO];
229         [self doneProcessingIndex];
232 - (void) readCachedFiles:(NSNotification *)notification
234         NSArray *lines = [self linesFromNotification:notification];
235         [self addFilesFromLines:lines cached:YES];
236         [self doneProcessingIndex];
239 - (void) commitFailedBecause:(NSString *)reason
241         self.busy--;
242         self.status = [@"Commit failed: " stringByAppendingString:reason];
243         [[NSAlert alertWithMessageText:@"Commit failed"
244                                          defaultButton:nil
245                                    alternateButton:nil
246                                            otherButton:nil
247                  informativeTextWithFormat:reason] runModal];
248         return;
251 - (IBAction) commit:(id) sender
253         if ([[cachedFilesController arrangedObjects] count] == 0) {
254                 [[NSAlert alertWithMessageText:@"No changes to commit"
255                                                  defaultButton:nil
256                                            alternateButton:nil
257                                                    otherButton:nil
258                          informativeTextWithFormat:@"You must first stage some changes before committing"] runModal];
259                 return;
260         }               
261         
262         NSString *commitMessage = [commitMessageView string];
263         if ([commitMessage length] < 3) {
264                 [[NSAlert alertWithMessageText:@"Commitmessage missing"
265                                                  defaultButton:nil
266                                            alternateButton:nil
267                                                    otherButton:nil
268                          informativeTextWithFormat:@"Please enter a commit message before committing"] runModal];
269                 return;
270         }
272         [cachedFilesController setSelectionIndexes:[NSIndexSet indexSet]];
273         [unstagedFilesController setSelectionIndexes:[NSIndexSet indexSet]];
275         NSString *commitSubject;
276         NSRange newLine = [commitMessage rangeOfString:@"\n"];
277         if (newLine.location == NSNotFound)
278                 commitSubject = commitMessage;
279         else
280                 commitSubject = [commitMessage substringToIndex:newLine.location];
281         
282         commitSubject = [@"commit: " stringByAppendingString:commitSubject];
284         self.busy++;
285         self.status = @"Creating tree..";
286         NSString *tree = [repository outputForCommand:@"write-tree"];
287         if ([tree length] != 40)
288                 return [self commitFailedBecause:@"Could not create a tree"];
290         int ret;
292         NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil];
293         NSString *parent = amend ? @"HEAD^" : @"HEAD";
294         if ([repository parseReference:parent]) {
295                 [arguments addObject:@"-p"];
296                 [arguments addObject:parent];
297         }
299         NSString *commit = [repository outputForArguments:arguments
300                                                                                   inputString:commitMessage
301                                                                                          retValue: &ret];
303         if (ret || [commit length] != 40)
304                 return [self commitFailedBecause:@"Could not create a commit object"];
305         
306         [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
307                                                   retValue: &ret];
308         if (ret)
309                 return [self commitFailedBecause:@"Could not update HEAD"];
311         [webController setStateMessage:[NSString stringWithFormat:@"Successfully created commit %@", commit]];
312         
313         repository.hasChanged = YES;
314         self.busy--;
315         [commitMessageView setString:@""];
316         amend = NO;
317         [self refresh:self];
318         self.amend = NO;
321 - (void) stageHunk:(NSString *)hunk reverse:(BOOL)reverse
323         NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", @"--cached", nil];
324         if (reverse)
325                 [array addObject:@"--reverse"];
327         int ret;
328         NSString *error = [repository outputForArguments:array
329                                                                                  inputString:hunk
330                                                                                         retValue: &ret];
331         if (ret)
332                 NSLog(@"Error: %@", error);
333         [self refresh:self]; // TODO: We should do this smarter by checking if the file diff is empty, which is faster.
335 @end