Merge commit 'origin/master'
[GitX.git] / PBGitIndexController.m
blob4d3cc787da942727a6a3adabc4fb490bdf33f8e4
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 @interface PBGitIndexController (PrivateMethods)
16 - (void)stopTrackingIndex;
17 - (void)resumeTrackingIndex;
18 @end
20 @implementation PBGitIndexController
22 @synthesize contextSize;
24 - (void)awakeFromNib
26         contextSize = 3;
28         [unstagedTable setDoubleAction:@selector(tableClicked:)];
29         [stagedTable setDoubleAction:@selector(tableClicked:)];
31         [unstagedTable setTarget:self];
32         [stagedTable setTarget:self];
34         [unstagedTable registerForDraggedTypes: [NSArray arrayWithObject:FileChangesTableViewType]];
35         [stagedTable registerForDraggedTypes: [NSArray arrayWithObject:FileChangesTableViewType]];
39 - (void) stageFiles:(NSArray *)files
41         NSMutableString *input = [NSMutableString string];
43         for (PBChangedFile *file in files) {
44                 [input appendFormat:@"%@\0", file.path];
45         }
47         int ret = 1;
48         [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
49          inputString:input retValue:&ret];
51         if (ret)
52         {
53                 NSLog(@"Error when updating index. Retvalue: %i", ret);
54                 return;
55         }
57         [self stopTrackingIndex];
58         for (PBChangedFile *file in files)
59         {
60                 file.hasUnstagedChanges = NO;
61                 file.hasStagedChanges = YES;
62         }
63         [self resumeTrackingIndex];
66 - (void) unstageFiles:(NSArray *)files
68         NSMutableString *input = [NSMutableString string];
69         
70         for (PBChangedFile *file in files) {
71                 [input appendString:[file indexInfo]];
72         }
74         int ret = 1;
75         [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
76          inputString:input retValue:&ret];
77         
78         if (ret)
79         {
80                 NSLog(@"Error when updating index. Retvalue: %i", ret);
81                 return;
82         }
84         [self stopTrackingIndex];
85         for (PBChangedFile *file in files)
86         {
87                 file.hasUnstagedChanges = YES;
88                 file.hasStagedChanges = NO;
89         }
90         [self resumeTrackingIndex];
93 - (void) ignoreFiles:(NSArray *)files
95         // Build output string
96         NSMutableArray *fileList = [NSMutableArray array];
97         for (PBChangedFile *file in files) {
98                 NSString *name = file.path;
99                 if ([name length] > 0)
100                         [fileList addObject:name];
101         }
102         NSString *filesAsString = [fileList componentsJoinedByString:@"\n"];
104         // Write to the file
105         NSString *gitIgnoreName = [commitController.repository gitIgnoreFilename];
107         NSStringEncoding enc = NSUTF8StringEncoding;
108         NSError *error = nil;
109         NSMutableString *ignoreFile;
111         if (![[NSFileManager defaultManager] fileExistsAtPath:gitIgnoreName]) {
112                 ignoreFile = [filesAsString mutableCopy];
113         } else {
114                 ignoreFile = [NSMutableString stringWithContentsOfFile:gitIgnoreName usedEncoding:&enc error:&error];
115                 if (error) {
116                         [[commitController.repository windowController] showErrorSheet:error];
117                         return;
118                 }
119                 // Add a newline if not yet present
120                 if ([ignoreFile characterAtIndex:([ignoreFile length] - 1)] != '\n')
121                         [ignoreFile appendString:@"\n"];
122                 [ignoreFile appendString:filesAsString];
123         }
125         [ignoreFile writeToFile:gitIgnoreName atomically:YES encoding:enc error:&error];
126         if (error)
127                 [[commitController.repository windowController] showErrorSheet:error];
130 # pragma mark Displaying diffs
132 - (NSString *) stagedChangesForFile:(PBChangedFile *)file
134         NSString *indexPath = [@":0:" stringByAppendingString:file.path];
136         if (file.status == NEW)
137                 return [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]];
139         return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", [self contextParameter], @"--cached", [commitController parentTree], @"--", file.path, nil]];
142 - (NSString *)unstagedChangesForFile:(PBChangedFile *)file
144         if (file.status == NEW) {
145                 NSStringEncoding encoding;
146                 NSError *error = nil;
147                 NSString *path = [[commitController.repository workingDirectory] stringByAppendingPathComponent:file.path];
148                 NSString *contents = [NSString stringWithContentsOfFile:path
149                                                                                                    usedEncoding:&encoding
150                                                                                                                   error:&error];
151                 if (error)
152                         return nil;
154                 return contents;
155         }
157         return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", [self contextParameter], @"--", file.path, nil]];
160 - (void)discardChangesForFiles:(NSArray *)files force:(BOOL)force
162         if(!force) {
163                 int ret = [[NSAlert alertWithMessageText:@"Discard changes"
164                                            defaultButton:nil
165                                          alternateButton:@"Cancel"
166                                              otherButton:nil
167                                informativeTextWithFormat:@"Are you sure you wish to discard the changes to this file?\n\nYou cannot undo this operation."] runModal];
168                 if (ret != NSAlertDefaultReturn)
169                         return;
170         }
172         NSArray *paths = [files valueForKey:@"path"];
173         NSString *input = [paths componentsJoinedByString:@"\0"];
175         NSArray *arguments = [NSArray arrayWithObjects:@"checkout-index", @"--index", @"--quiet", @"--force", @"-z", @"--stdin", nil];
176         int ret = 1;
177         [commitController.repository outputForArguments:arguments inputString:input retValue:&ret];
178         if (ret) {
179                 [[commitController.repository windowController] showMessageSheet:@"Discarding changes failed" infoText:[NSString stringWithFormat:@"Discarding changes failed with error code %i", ret]];
180                 return;
181         }
183         for (PBChangedFile *file in files)
184                 file.hasUnstagedChanges = NO;
187 # pragma mark Context Menu methods
188 - (BOOL) allSelectedCanBeIgnored:(NSArray *)selectedFiles
190         for (PBChangedFile *selectedItem in selectedFiles) {
191                 if (selectedItem.status != NEW) {
192                         return NO;
193                 }
194         }
195         return YES;
198 - (NSMenu *) menuForTable:(NSTableView *)table
200         NSMenu *menu = [[NSMenu alloc] init];
201         id controller = [table tag] == 0 ? unstagedFilesController : stagedFilesController;
202         NSArray *selectedFiles = [controller selectedObjects];
204         // Unstaged changes
205         if ([table tag] == 0) {
206                 NSMenuItem *stageItem = [[NSMenuItem alloc] initWithTitle:@"Stage Changes" action:@selector(stageFilesAction:) keyEquivalent:@""];
207                 [stageItem setTarget:self];
208                 [stageItem setRepresentedObject:selectedFiles];
209                 [menu addItem:stageItem];
210         }
211         else if ([table tag] == 1) {
212                 NSMenuItem *unstageItem = [[NSMenuItem alloc] initWithTitle:@"Unstage Changes" action:@selector(unstageFilesAction:) keyEquivalent:@""];
213                 [unstageItem setTarget:self];
214                 [unstageItem setRepresentedObject:selectedFiles];
215                 [menu addItem:unstageItem];
216         }
218         NSString *title = [selectedFiles count] == 1 ? @"Open file" : @"Open files";
219         NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(openFilesAction:) keyEquivalent:@""];
220         [openItem setTarget:self];
221         [openItem setRepresentedObject:selectedFiles];
222         [menu addItem:openItem];
224         // Attempt to ignore
225         if ([self allSelectedCanBeIgnored:selectedFiles]) {
226                 NSString *ignoreText = [selectedFiles count] == 1 ? @"Ignore File": @"Ignore Files";
227                 NSMenuItem *ignoreItem = [[NSMenuItem alloc] initWithTitle:ignoreText action:@selector(ignoreFilesAction:) keyEquivalent:@""];
228                 [ignoreItem setTarget:self];
229                 [ignoreItem setRepresentedObject:selectedFiles];
230                 [menu addItem:ignoreItem];
231         }
233         if ([selectedFiles count] == 1) {
234                 NSMenuItem *showInFinderItem = [[NSMenuItem alloc] initWithTitle:@"Show in Finder" action:@selector(showInFinderAction:) keyEquivalent:@""];
235                 [showInFinderItem setTarget:self];
236                 [showInFinderItem setRepresentedObject:selectedFiles];
237                 [menu addItem:showInFinderItem];
238     }
240         for (PBChangedFile *file in selectedFiles)
241                 if (!file.hasUnstagedChanges)
242                         return menu;
244         NSMenuItem *discardItem = [[NSMenuItem alloc] initWithTitle:@"Discard changes" action:@selector(discardFilesAction:) keyEquivalent:@""];
245         [discardItem setTarget:self];
246         [discardItem setAlternate:NO];
247         [discardItem setRepresentedObject:selectedFiles];
249         [menu addItem:discardItem];
251         NSMenuItem *discardForceItem = [[NSMenuItem alloc] initWithTitle:@"Discard changes" action:@selector(forceDiscardFilesAction:) keyEquivalent:@""];
252         [discardForceItem setTarget:self];
253         [discardForceItem setAlternate:YES];
254         [discardForceItem setRepresentedObject:selectedFiles];
255         [discardForceItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
256         [menu addItem:discardForceItem];
257         
258         return menu;
261 - (void) stageFilesAction:(id) sender
263         [self stageFiles:[sender representedObject]];
266 - (void) unstageFilesAction:(id) sender
268         [self unstageFiles:[sender representedObject]];
271 - (void) openFilesAction:(id) sender
273         NSArray *files = [sender representedObject];
274         NSString *workingDirectory = [commitController.repository workingDirectory];
275         for (PBChangedFile *file in files)
276                 [[NSWorkspace sharedWorkspace] openFile:[workingDirectory stringByAppendingPathComponent:[file path]]];
279 - (void) ignoreFilesAction:(id) sender
281         NSArray *selectedFiles = [sender representedObject];
282         if ([selectedFiles count] > 0) {
283                 [self ignoreFiles:selectedFiles];
284         }
285         [commitController refresh:NULL];
288 - (void)discardFilesAction:(id) sender
290         NSArray *selectedFiles = [sender representedObject];
291         if ([selectedFiles count] > 0)
292                 [self discardChangesForFiles:selectedFiles force:FALSE];
295 - (void)forceDiscardFilesAction:(id) sender
297         NSArray *selectedFiles = [sender representedObject];
298         if ([selectedFiles count] > 0)
299                 [self discardChangesForFiles:selectedFiles force:TRUE];
302 - (void) showInFinderAction:(id) sender
304         NSArray *selectedFiles = [sender representedObject];
305         if ([selectedFiles count] == 0)
306                 return;
307         NSString *workingDirectory = [[commitController.repository workingDirectory] stringByAppendingString:@"/"];
308         NSString *path = [workingDirectory stringByAppendingPathComponent:[[selectedFiles objectAtIndex:0] path]];
309         NSWorkspace *ws = [NSWorkspace sharedWorkspace];
310         [ws selectFile: path inFileViewerRootedAtPath:nil];
314 # pragma mark TableView icon delegate
315 - (void)tableView:(NSTableView*)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex
317         id controller = [tableView tag] == 0 ? unstagedFilesController : stagedFilesController;
318         [[tableColumn dataCell] setImage:[[[controller arrangedObjects] objectAtIndex:rowIndex] icon]];
321 - (void) tableClicked:(NSTableView *) tableView
323         NSArrayController *controller = [tableView tag] == 0 ? unstagedFilesController : stagedFilesController;
325         NSIndexSet *selectionIndexes = [tableView selectedRowIndexes];
326         NSArray *files = [[controller arrangedObjects] objectsAtIndexes:selectionIndexes];
327         if ([tableView tag] == 0)
328                 [self stageFiles:files];
329         else
330                 [self unstageFiles:files];
333 - (void) rowClicked:(NSCell *)sender
335         NSTableView *tableView = (NSTableView *)[sender controlView];
336         if([tableView numberOfSelectedRows] != 1)
337                 return;
338         [self tableClicked: tableView];
341 - (BOOL)tableView:(NSTableView *)tv
342 writeRowsWithIndexes:(NSIndexSet *)rowIndexes
343          toPasteboard:(NSPasteboard*)pboard
345     // Copy the row numbers to the pasteboard.
346     [pboard declareTypes:[NSArray arrayWithObjects:FileChangesTableViewType, NSFilenamesPboardType, nil] owner:self];
348         // Internal, for dragging from one tableview to the other
349         NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
350     [pboard setData:data forType:FileChangesTableViewType];
352         // External, to drag them to for example XCode or Textmate
353         NSArrayController *controller = [tv tag] == 0 ? unstagedFilesController : stagedFilesController;
354         NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes];
355         NSString *workingDirectory = [commitController.repository workingDirectory];
357         NSMutableArray *filenames = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
358         for (PBChangedFile *file in files)
359                 [filenames addObject:[workingDirectory stringByAppendingPathComponent:[file path]]];
361         [pboard setPropertyList:filenames forType:NSFilenamesPboardType];
362     return YES;
365 - (NSDragOperation)tableView:(NSTableView*)tableView
366                                 validateDrop:(id <NSDraggingInfo>)info
367                                  proposedRow:(NSInteger)row
368            proposedDropOperation:(NSTableViewDropOperation)operation
370         if ([info draggingSource] == tableView)
371                 return NSDragOperationNone;
373         [tableView setDropRow:-1 dropOperation:NSTableViewDropOn];
374     return NSDragOperationCopy;
377 - (BOOL)tableView:(NSTableView *)aTableView
378            acceptDrop:(id <NSDraggingInfo>)info
379                           row:(NSInteger)row
380         dropOperation:(NSTableViewDropOperation)operation
382     NSPasteboard* pboard = [info draggingPasteboard];
383     NSData* rowData = [pboard dataForType:FileChangesTableViewType];
384     NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
386         NSArrayController *controller = [aTableView tag] == 0 ? stagedFilesController : unstagedFilesController;
387         NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes];
389         if ([aTableView tag] == 0)
390                 [self unstageFiles:files];
391         else
392                 [self stageFiles:files];
394         return YES;
397 - (NSString *) contextParameter
399         return [[NSString alloc] initWithFormat:@"-U%i", contextSize];
402 # pragma mark WebKit Accessibility
404 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
406         return NO;
409 #pragma mark Private Methods
410 - (void)stopTrackingIndex
412         [stagedFilesController setAutomaticallyRearrangesObjects:NO];
413         [unstagedFilesController setAutomaticallyRearrangesObjects:NO];
415 - (void)resumeTrackingIndex
417         [stagedFilesController setAutomaticallyRearrangesObjects:YES];
418         [unstagedFilesController setAutomaticallyRearrangesObjects:YES];
419         [stagedFilesController rearrangeObjects];
420         [unstagedFilesController rearrangeObjects];
422 @end