2 // PBGitIndexController.m
5 // Created by Pieter de Bie on 18-11-08.
6 // Copyright 2008 Pieter de Bie. All rights reserved.
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;
20 @implementation PBGitIndexController
22 @synthesize contextSize;
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];
48 [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
49 inputString:input retValue:&ret];
53 NSLog(@"Error when updating index. Retvalue: %i", ret);
57 [self stopTrackingIndex];
58 for (PBChangedFile *file in files)
60 file.hasUnstagedChanges = NO;
61 file.hasStagedChanges = YES;
63 [self resumeTrackingIndex];
66 - (void) unstageFiles:(NSArray *)files
68 NSMutableString *input = [NSMutableString string];
70 for (PBChangedFile *file in files) {
71 [input appendString:[file indexInfo]];
75 [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
76 inputString:input retValue:&ret];
80 NSLog(@"Error when updating index. Retvalue: %i", ret);
84 [self stopTrackingIndex];
85 for (PBChangedFile *file in files)
87 file.hasUnstagedChanges = YES;
88 file.hasStagedChanges = NO;
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];
102 NSString *filesAsString = [fileList componentsJoinedByString:@"\n"];
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];
114 ignoreFile = [NSMutableString stringWithContentsOfFile:gitIgnoreName usedEncoding:&enc error:&error];
116 [[commitController.repository windowController] showErrorSheet:error];
119 // Add a newline if not yet present
120 if ([ignoreFile characterAtIndex:([ignoreFile length] - 1)] != '\n')
121 [ignoreFile appendString:@"\n"];
122 [ignoreFile appendString:filesAsString];
125 [ignoreFile writeToFile:gitIgnoreName atomically:YES encoding:enc error:&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
157 return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", [self contextParameter], @"--", file.path, nil]];
160 - (void)discardChangesForFiles:(NSArray *)files force:(BOOL)force
163 int ret = [[NSAlert alertWithMessageText:@"Discard changes"
165 alternateButton:@"Cancel"
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)
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];
177 [commitController.repository outputForArguments:arguments inputString:input retValue:&ret];
179 [[commitController.repository windowController] showMessageSheet:@"Discarding changes failed" infoText:[NSString stringWithFormat:@"Discarding changes failed with error code %i", ret]];
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) {
198 - (NSMenu *) menuForTable:(NSTableView *)table
200 NSMenu *menu = [[NSMenu alloc] init];
201 id controller = [table tag] == 0 ? unstagedFilesController : stagedFilesController;
202 NSArray *selectedFiles = [controller selectedObjects];
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];
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];
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];
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];
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];
240 for (PBChangedFile *file in selectedFiles)
241 if (!file.hasUnstagedChanges)
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];
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];
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)
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];
330 [self unstageFiles:files];
333 - (void) rowClicked:(NSCell *)sender
335 NSTableView *tableView = (NSTableView *)[sender controlView];
336 if([tableView numberOfSelectedRows] != 1)
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];
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
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];
392 [self stageFiles:files];
397 - (NSString *) contextParameter
399 return [[NSString alloc] initWithFormat:@"-U%i", contextSize];
402 # pragma mark WebKit Accessibility
404 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
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];