HistoryView: Don't show the 'loading commit' thing until after 500 ms.
[GitX.git] / PBGitIndexController.m
blob6645ca12646c8d27533116d4627173480ecb64d4
1 //
2 //  PBGitIndexController.m
3 //  GitX
4 //
5 //  Created by Pieter de Bie on 18-11-08.
6 //  Copyright 2008 Pieter de Bie. All rights reserved.
7 //
9 #import "PBGitIndexController.h"
10 #import "PBChangedFile.h"
11 #import "PBGitRepository.h"
13 #define FileChangesTableViewType @"GitFileChangedType"
15 @implementation PBGitIndexController
17 @synthesize contextSize;
19 - (void)awakeFromNib
21         contextSize = 3;
23         [unstagedTable setDoubleAction:@selector(tableClicked:)];
24         [stagedTable setDoubleAction:@selector(tableClicked:)];
26         [unstagedTable setTarget:self];
27         [stagedTable setTarget:self];
29         [unstagedTable registerForDraggedTypes: [NSArray arrayWithObject:FileChangesTableViewType]];
30         [stagedTable registerForDraggedTypes: [NSArray arrayWithObject:FileChangesTableViewType]];
34 - (void) stageFiles:(NSArray *)files
36         NSMutableString *input = [NSMutableString string];
38         for (PBChangedFile *file in files) {
39                 [input appendFormat:@"%@\0", file.path];
40         }
42         int ret = 1;
43         [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
44          inputString:input retValue:&ret];
46         if (ret)
47         {
48                 NSLog(@"Error when updating index. Retvalue: %i", ret);
49                 return;
50         }
52         [self stopTrackingIndex];
53         for (PBChangedFile *file in files)
54         {
55                 file.hasUnstagedChanges = NO;
56                 file.hasStagedChanges = YES;
57         }
58         [self resumeTrackingIndex];
61 - (void) unstageFiles:(NSArray *)files
63         NSMutableString *input = [NSMutableString string];
64         
65         for (PBChangedFile *file in files) {
66                 [input appendString:[file indexInfo]];
67         }
69         int ret = 1;
70         [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
71          inputString:input retValue:&ret];
72         
73         if (ret)
74         {
75                 NSLog(@"Error when updating index. Retvalue: %i", ret);
76                 return;
77         }
79         [self stopTrackingIndex];
80         for (PBChangedFile *file in files)
81         {
82                 file.hasUnstagedChanges = YES;
83                 file.hasStagedChanges = NO;
84         }
85         [self resumeTrackingIndex];
88 - (void) ignoreFiles:(NSArray *)files
90         // Build output string
91         NSMutableArray *fileList = [NSMutableArray array];
92         for (PBChangedFile *file in files) {
93                 NSString *name = file.path;
94                 if ([name length] > 0)
95                         [fileList addObject:name];
96         }
97         NSString *filesAsString = [fileList componentsJoinedByString:@"\n"];
99         // Write to the file
100         NSString *gitIgnoreName = [commitController.repository gitIgnoreFilename];
102         NSStringEncoding enc = NSUTF8StringEncoding;
103         NSError *error = nil;
104         NSMutableString *ignoreFile;
106         if (![[NSFileManager defaultManager] fileExistsAtPath:gitIgnoreName]) {
107                 ignoreFile = [filesAsString mutableCopy];
108         } else {
109                 ignoreFile = [NSMutableString stringWithContentsOfFile:gitIgnoreName usedEncoding:&enc error:&error];
110                 if (error) {
111                         [[commitController.repository windowController] showErrorSheet:error];
112                         return;
113                 }
114                 // Add a newline if not yet present
115                 if ([ignoreFile characterAtIndex:([ignoreFile length] - 1)] != '\n')
116                         [ignoreFile appendString:@"\n"];
117                 [ignoreFile appendString:filesAsString];
118         }
120         [ignoreFile writeToFile:gitIgnoreName atomically:YES encoding:enc error:&error];
121         if (error)
122                 [[commitController.repository windowController] showErrorSheet:error];
125 # pragma mark Displaying diffs
127 - (NSString *) stagedChangesForFile:(PBChangedFile *)file
129         NSString *indexPath = [@":0:" stringByAppendingString:file.path];
131         if (file.status == NEW)
132                 return [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]];
134         return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", [self contextParameter], @"--cached", [commitController parentTree], @"--", file.path, nil]];
137 - (NSString *)unstagedChangesForFile:(PBChangedFile *)file
139         if (file.status == NEW) {
140                 NSStringEncoding encoding;
141                 NSError *error = nil;
142                 NSString *path = [[commitController.repository workingDirectory] stringByAppendingPathComponent:file.path];
143                 NSString *contents = [NSString stringWithContentsOfFile:path
144                                                                                                    usedEncoding:&encoding
145                                                                                                                   error:&error];
146                 if (error)
147                         return nil;
149                 return contents;
150         }
152         return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", [self contextParameter], @"--", file.path, nil]];
155 - (void)discardChangesForFiles:(NSArray *)files force:(BOOL)force
157         if(!force) {
158                 int ret = [[NSAlert alertWithMessageText:@"Discard changes"
159                                            defaultButton:nil
160                                          alternateButton:@"Cancel"
161                                              otherButton:nil
162                                informativeTextWithFormat:@"Are you sure you wish to discard the changes to this file?\n\nYou cannot undo this operation."] runModal];
163                 if (ret != NSAlertDefaultReturn)
164                         return;
165         }
167         NSArray *paths = [files valueForKey:@"path"];
168         NSString *input = [paths componentsJoinedByString:@"\0"];
170         NSArray *arguments = [NSArray arrayWithObjects:@"checkout-index", @"--index", @"--quiet", @"--force", @"-z", @"--stdin", nil];
171         int ret = 1;
172         [commitController.repository outputForArguments:arguments inputString:input retValue:&ret];
173         if (ret) {
174                 [[commitController.repository windowController] showMessageSheet:@"Discarding changes failed" infoText:[NSString stringWithFormat:@"Discarding changes failed with error code %i", ret]];
175                 return;
176         }
178         for (PBChangedFile *file in files)
179                 file.hasUnstagedChanges = NO;
182 # pragma mark Context Menu methods
183 - (BOOL) allSelectedCanBeIgnored:(NSArray *)selectedFiles
185         for (PBChangedFile *selectedItem in selectedFiles) {
186                 if (selectedItem.status != NEW) {
187                         return NO;
188                 }
189         }
190         return YES;
193 - (NSMenu *) menuForTable:(NSTableView *)table
195         NSMenu *menu = [[NSMenu alloc] init];
196         id controller = [table tag] == 0 ? unstagedFilesController : stagedFilesController;
197         NSArray *selectedFiles = [controller selectedObjects];
199         // Unstaged changes
200         if ([table tag] == 0) {
201                 NSMenuItem *stageItem = [[NSMenuItem alloc] initWithTitle:@"Stage Changes" action:@selector(stageFilesAction:) keyEquivalent:@""];
202                 [stageItem setTarget:self];
203                 [stageItem setRepresentedObject:selectedFiles];
204                 [menu addItem:stageItem];
205         }
206         else if ([table tag] == 1) {
207                 NSMenuItem *unstageItem = [[NSMenuItem alloc] initWithTitle:@"Unstage Changes" action:@selector(unstageFilesAction:) keyEquivalent:@""];
208                 [unstageItem setTarget:self];
209                 [unstageItem setRepresentedObject:selectedFiles];
210                 [menu addItem:unstageItem];
211         }
213         NSString *title = [selectedFiles count] == 1 ? @"Open file" : @"Open files";
214         NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(openFilesAction:) keyEquivalent:@""];
215         [openItem setTarget:self];
216         [openItem setRepresentedObject:selectedFiles];
217         [menu addItem:openItem];
219         // Attempt to ignore
220         if ([self allSelectedCanBeIgnored:selectedFiles]) {
221                 NSString *ignoreText = [selectedFiles count] == 1 ? @"Ignore File": @"Ignore Files";
222                 NSMenuItem *ignoreItem = [[NSMenuItem alloc] initWithTitle:ignoreText action:@selector(ignoreFilesAction:) keyEquivalent:@""];
223                 [ignoreItem setTarget:self];
224                 [ignoreItem setRepresentedObject:selectedFiles];
225                 [menu addItem:ignoreItem];
226         }
228         if ([selectedFiles count] == 1) {
229                 NSMenuItem *showInFinderItem = [[NSMenuItem alloc] initWithTitle:@"Show in Finder" action:@selector(showInFinderAction:) keyEquivalent:@""];
230                 [showInFinderItem setTarget:self];
231                 [showInFinderItem setRepresentedObject:selectedFiles];
232                 [menu addItem:showInFinderItem];
233     }
235         for (PBChangedFile *file in selectedFiles)
236                 if (!file.hasUnstagedChanges)
237                         return menu;
239         NSMenuItem *discardItem = [[NSMenuItem alloc] initWithTitle:@"Discard changes…" action:@selector(discardFilesAction:) keyEquivalent:@""];
240         [discardItem setTarget:self];
241         [discardItem setAlternate:NO];
242         [discardItem setRepresentedObject:selectedFiles];
244         [menu addItem:discardItem];
246         NSMenuItem *discardForceItem = [[NSMenuItem alloc] initWithTitle:@"Discard changes" action:@selector(forceDiscardFilesAction:) keyEquivalent:@""];
247         [discardForceItem setTarget:self];
248         [discardForceItem setAlternate:YES];
249         [discardForceItem setRepresentedObject:selectedFiles];
250         [discardForceItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
251         [menu addItem:discardForceItem];
252         
253         return menu;
256 - (void) stageFilesAction:(id) sender
258         [self stageFiles:[sender representedObject]];
261 - (void) unstageFilesAction:(id) sender
263         [self unstageFiles:[sender representedObject]];
266 - (void) openFilesAction:(id) sender
268         NSArray *files = [sender representedObject];
269         NSString *workingDirectory = [commitController.repository workingDirectory];
270         for (PBChangedFile *file in files)
271                 [[NSWorkspace sharedWorkspace] openFile:[workingDirectory stringByAppendingPathComponent:[file path]]];
274 - (void) ignoreFilesAction:(id) sender
276         NSArray *selectedFiles = [sender representedObject];
277         if ([selectedFiles count] > 0) {
278                 [self ignoreFiles:selectedFiles];
279         }
280         [commitController refresh:NULL];
283 - (void)discardFilesAction:(id) sender
285         NSArray *selectedFiles = [sender representedObject];
286         if ([selectedFiles count] > 0)
287                 [self discardChangesForFiles:selectedFiles force:FALSE];
290 - (void)forceDiscardFilesAction:(id) sender
292         NSArray *selectedFiles = [sender representedObject];
293         if ([selectedFiles count] > 0)
294                 [self discardChangesForFiles:selectedFiles force:TRUE];
297 - (void) showInFinderAction:(id) sender
299         NSArray *selectedFiles = [sender representedObject];
300         if ([selectedFiles count] == 0)
301                 return;
302         NSString *workingDirectory = [[commitController.repository workingDirectory] stringByAppendingString:@"/"];
303         NSString *path = [workingDirectory stringByAppendingPathComponent:[[selectedFiles objectAtIndex:0] path]];
304         NSWorkspace *ws = [NSWorkspace sharedWorkspace];
305         [ws selectFile: path inFileViewerRootedAtPath:nil];
309 # pragma mark TableView icon delegate
310 - (void)tableView:(NSTableView*)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex
312         id controller = [tableView tag] == 0 ? unstagedFilesController : stagedFilesController;
313         [[tableColumn dataCell] setImage:[[[controller arrangedObjects] objectAtIndex:rowIndex] icon]];
316 - (void) tableClicked:(NSTableView *) tableView
318         NSArrayController *controller = [tableView tag] == 0 ? unstagedFilesController : stagedFilesController;
320         NSIndexSet *selectionIndexes = [tableView selectedRowIndexes];
321         NSArray *files = [[controller arrangedObjects] objectsAtIndexes:selectionIndexes];
322         if ([tableView tag] == 0)
323                 [self stageFiles:files];
324         else
325                 [self unstageFiles:files];
328 - (void) rowClicked:(NSCell *)sender
330         NSTableView *tableView = (NSTableView *)[sender controlView];
331         if([tableView numberOfSelectedRows] != 1)
332                 return;
333         [self tableClicked: tableView];
336 - (BOOL)tableView:(NSTableView *)tv
337 writeRowsWithIndexes:(NSIndexSet *)rowIndexes
338          toPasteboard:(NSPasteboard*)pboard
340     // Copy the row numbers to the pasteboard.
341     [pboard declareTypes:[NSArray arrayWithObjects:FileChangesTableViewType, NSFilenamesPboardType, nil] owner:self];
343         // Internal, for dragging from one tableview to the other
344         NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
345     [pboard setData:data forType:FileChangesTableViewType];
347         // External, to drag them to for example XCode or Textmate
348         NSArrayController *controller = [tv tag] == 0 ? unstagedFilesController : stagedFilesController;
349         NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes];
350         NSString *workingDirectory = [commitController.repository workingDirectory];
352         NSMutableArray *filenames = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
353         for (PBChangedFile *file in files)
354                 [filenames addObject:[workingDirectory stringByAppendingPathComponent:[file path]]];
356         [pboard setPropertyList:filenames forType:NSFilenamesPboardType];
357     return YES;
360 - (NSDragOperation)tableView:(NSTableView*)tableView
361                                 validateDrop:(id <NSDraggingInfo>)info
362                                  proposedRow:(NSInteger)row
363            proposedDropOperation:(NSTableViewDropOperation)operation
365         if ([info draggingSource] == tableView)
366                 return NSDragOperationNone;
368         [tableView setDropRow:-1 dropOperation:NSTableViewDropOn];
369     return NSDragOperationCopy;
372 - (BOOL)tableView:(NSTableView *)aTableView
373            acceptDrop:(id <NSDraggingInfo>)info
374                           row:(NSInteger)row
375         dropOperation:(NSTableViewDropOperation)operation
377     NSPasteboard* pboard = [info draggingPasteboard];
378     NSData* rowData = [pboard dataForType:FileChangesTableViewType];
379     NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
381         NSArrayController *controller = [aTableView tag] == 0 ? stagedFilesController : unstagedFilesController;
382         NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes];
384         if ([aTableView tag] == 0)
385                 [self unstageFiles:files];
386         else
387                 [self stageFiles:files];
389         return YES;
392 - (NSString *) contextParameter
394         return [[NSString alloc] initWithFormat:@"-U%i", contextSize];
397 # pragma mark WebKit Accessibility
399 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
401         return NO;
404 #pragma mark Private Methods
405 - (void)stopTrackingIndex
407         [stagedFilesController setAutomaticallyRearrangesObjects:NO];
408         [unstagedFilesController setAutomaticallyRearrangesObjects:NO];
410 - (void)resumeTrackingIndex
412         [stagedFilesController setAutomaticallyRearrangesObjects:YES];
413         [unstagedFilesController setAutomaticallyRearrangesObjects:YES];
414         [stagedFilesController rearrangeObjects];
415         [unstagedFilesController rearrangeObjects];
417 @end