// // PBGitIndexController.m // GitX // // Created by Pieter de Bie on 18-11-08. // Copyright 2008 Pieter de Bie. All rights reserved. // #import "PBGitIndexController.h" #import "PBChangedFile.h" #import "PBGitRepository.h" #define FileChangesTableViewType @"GitFileChangedType" @interface PBGitIndexController (PrivateMethods) - (void)stopTrackingIndex; - (void)resumeTrackingIndex; @end @implementation PBGitIndexController @synthesize contextSize; - (void)awakeFromNib { contextSize = 3; [unstagedTable setDoubleAction:@selector(tableClicked:)]; [stagedTable setDoubleAction:@selector(tableClicked:)]; [unstagedTable setTarget:self]; [stagedTable setTarget:self]; [unstagedTable registerForDraggedTypes: [NSArray arrayWithObject:FileChangesTableViewType]]; [stagedTable registerForDraggedTypes: [NSArray arrayWithObject:FileChangesTableViewType]]; } - (void) stageFiles:(NSArray *)files { NSMutableString *input = [NSMutableString string]; for (PBChangedFile *file in files) { [input appendFormat:@"%@\0", file.path]; } int ret = 1; [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil] inputString:input retValue:&ret]; if (ret) { NSLog(@"Error when updating index. Retvalue: %i", ret); return; } [self stopTrackingIndex]; for (PBChangedFile *file in files) { file.hasUnstagedChanges = NO; file.hasStagedChanges = YES; } [self resumeTrackingIndex]; } - (void) unstageFiles:(NSArray *)files { NSMutableString *input = [NSMutableString string]; for (PBChangedFile *file in files) { [input appendString:[file indexInfo]]; } int ret = 1; [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil] inputString:input retValue:&ret]; if (ret) { NSLog(@"Error when updating index. Retvalue: %i", ret); return; } [self stopTrackingIndex]; for (PBChangedFile *file in files) { file.hasUnstagedChanges = YES; file.hasStagedChanges = NO; } [self resumeTrackingIndex]; } - (void) ignoreFiles:(NSArray *)files { // Build output string NSMutableArray *fileList = [NSMutableArray array]; for (PBChangedFile *file in files) { NSString *name = file.path; if ([name length] > 0) [fileList addObject:name]; } NSString *filesAsString = [fileList componentsJoinedByString:@"\n"]; // Write to the file NSString *gitIgnoreName = [commitController.repository gitIgnoreFilename]; NSStringEncoding enc = NSUTF8StringEncoding; NSError *error = nil; NSMutableString *ignoreFile; if (![[NSFileManager defaultManager] fileExistsAtPath:gitIgnoreName]) { ignoreFile = [filesAsString mutableCopy]; } else { ignoreFile = [NSMutableString stringWithContentsOfFile:gitIgnoreName usedEncoding:&enc error:&error]; if (error) { [[commitController.repository windowController] showErrorSheet:error]; return; } // Add a newline if not yet present if ([ignoreFile characterAtIndex:([ignoreFile length] - 1)] != '\n') [ignoreFile appendString:@"\n"]; [ignoreFile appendString:filesAsString]; } [ignoreFile writeToFile:gitIgnoreName atomically:YES encoding:enc error:&error]; if (error) [[commitController.repository windowController] showErrorSheet:error]; } # pragma mark Displaying diffs - (NSString *) stagedChangesForFile:(PBChangedFile *)file { NSString *indexPath = [@":0:" stringByAppendingString:file.path]; if (file.status == NEW) return [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]]; return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", [self contextParameter], @"--cached", [commitController parentTree], @"--", file.path, nil]]; } - (NSString *)unstagedChangesForFile:(PBChangedFile *)file { if (file.status == NEW) { NSStringEncoding encoding; NSError *error = nil; NSString *path = [[commitController.repository workingDirectory] stringByAppendingPathComponent:file.path]; NSString *contents = [NSString stringWithContentsOfFile:path usedEncoding:&encoding error:&error]; if (error) return nil; return contents; } return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", [self contextParameter], @"--", file.path, nil]]; } - (void)discardChangesForFiles:(NSArray *)files force:(BOOL)force { if(!force) { int ret = [[NSAlert alertWithMessageText:@"Discard changes" defaultButton:nil alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@"Are you sure you wish to discard the changes to this file?\n\nYou cannot undo this operation."] runModal]; if (ret != NSAlertDefaultReturn) return; } NSArray *paths = [files valueForKey:@"path"]; NSString *input = [paths componentsJoinedByString:@"\0"]; NSArray *arguments = [NSArray arrayWithObjects:@"checkout-index", @"--index", @"--quiet", @"--force", @"-z", @"--stdin", nil]; int ret = 1; [commitController.repository outputForArguments:arguments inputString:input retValue:&ret]; if (ret) { [[commitController.repository windowController] showMessageSheet:@"Discarding changes failed" infoText:[NSString stringWithFormat:@"Discarding changes failed with error code %i", ret]]; return; } for (PBChangedFile *file in files) file.hasUnstagedChanges = NO; } # pragma mark Context Menu methods - (BOOL) allSelectedCanBeIgnored:(NSArray *)selectedFiles { for (PBChangedFile *selectedItem in selectedFiles) { if (selectedItem.status != NEW) { return NO; } } return YES; } - (NSMenu *) menuForTable:(NSTableView *)table { NSMenu *menu = [[NSMenu alloc] init]; id controller = [table tag] == 0 ? unstagedFilesController : stagedFilesController; NSArray *selectedFiles = [controller selectedObjects]; // Unstaged changes if ([table tag] == 0) { NSMenuItem *stageItem = [[NSMenuItem alloc] initWithTitle:@"Stage Changes" action:@selector(stageFilesAction:) keyEquivalent:@""]; [stageItem setTarget:self]; [stageItem setRepresentedObject:selectedFiles]; [menu addItem:stageItem]; } else if ([table tag] == 1) { NSMenuItem *unstageItem = [[NSMenuItem alloc] initWithTitle:@"Unstage Changes" action:@selector(unstageFilesAction:) keyEquivalent:@""]; [unstageItem setTarget:self]; [unstageItem setRepresentedObject:selectedFiles]; [menu addItem:unstageItem]; } NSString *title = [selectedFiles count] == 1 ? @"Open file" : @"Open files"; NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(openFilesAction:) keyEquivalent:@""]; [openItem setTarget:self]; [openItem setRepresentedObject:selectedFiles]; [menu addItem:openItem]; // Attempt to ignore if ([self allSelectedCanBeIgnored:selectedFiles]) { NSString *ignoreText = [selectedFiles count] == 1 ? @"Ignore File": @"Ignore Files"; NSMenuItem *ignoreItem = [[NSMenuItem alloc] initWithTitle:ignoreText action:@selector(ignoreFilesAction:) keyEquivalent:@""]; [ignoreItem setTarget:self]; [ignoreItem setRepresentedObject:selectedFiles]; [menu addItem:ignoreItem]; } if ([selectedFiles count] == 1) { NSMenuItem *showInFinderItem = [[NSMenuItem alloc] initWithTitle:@"Show in Finder" action:@selector(showInFinderAction:) keyEquivalent:@""]; [showInFinderItem setTarget:self]; [showInFinderItem setRepresentedObject:selectedFiles]; [menu addItem:showInFinderItem]; } for (PBChangedFile *file in selectedFiles) if (!file.hasUnstagedChanges) return menu; NSMenuItem *discardItem = [[NSMenuItem alloc] initWithTitle:@"Discard changes" action:@selector(discardFilesAction:) keyEquivalent:@""]; [discardItem setTarget:self]; [discardItem setAlternate:NO]; [discardItem setRepresentedObject:selectedFiles]; [menu addItem:discardItem]; NSMenuItem *discardForceItem = [[NSMenuItem alloc] initWithTitle:@"Discard changes" action:@selector(forceDiscardFilesAction:) keyEquivalent:@""]; [discardForceItem setTarget:self]; [discardForceItem setAlternate:YES]; [discardForceItem setRepresentedObject:selectedFiles]; [discardForceItem setKeyEquivalentModifierMask:NSAlternateKeyMask]; [menu addItem:discardForceItem]; return menu; } - (void) stageFilesAction:(id) sender { [self stageFiles:[sender representedObject]]; } - (void) unstageFilesAction:(id) sender { [self unstageFiles:[sender representedObject]]; } - (void) openFilesAction:(id) sender { NSArray *files = [sender representedObject]; NSString *workingDirectory = [commitController.repository workingDirectory]; for (PBChangedFile *file in files) [[NSWorkspace sharedWorkspace] openFile:[workingDirectory stringByAppendingPathComponent:[file path]]]; } - (void) ignoreFilesAction:(id) sender { NSArray *selectedFiles = [sender representedObject]; if ([selectedFiles count] > 0) { [self ignoreFiles:selectedFiles]; } [commitController refresh:NULL]; } - (void)discardFilesAction:(id) sender { NSArray *selectedFiles = [sender representedObject]; if ([selectedFiles count] > 0) [self discardChangesForFiles:selectedFiles force:FALSE]; } - (void)forceDiscardFilesAction:(id) sender { NSArray *selectedFiles = [sender representedObject]; if ([selectedFiles count] > 0) [self discardChangesForFiles:selectedFiles force:TRUE]; } - (void) showInFinderAction:(id) sender { NSArray *selectedFiles = [sender representedObject]; if ([selectedFiles count] == 0) return; NSString *workingDirectory = [[commitController.repository workingDirectory] stringByAppendingString:@"/"]; NSString *path = [workingDirectory stringByAppendingPathComponent:[[selectedFiles objectAtIndex:0] path]]; NSWorkspace *ws = [NSWorkspace sharedWorkspace]; [ws selectFile: path inFileViewerRootedAtPath:nil]; } # pragma mark TableView icon delegate - (void)tableView:(NSTableView*)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex { id controller = [tableView tag] == 0 ? unstagedFilesController : stagedFilesController; [[tableColumn dataCell] setImage:[[[controller arrangedObjects] objectAtIndex:rowIndex] icon]]; } - (void) tableClicked:(NSTableView *) tableView { NSArrayController *controller = [tableView tag] == 0 ? unstagedFilesController : stagedFilesController; NSIndexSet *selectionIndexes = [tableView selectedRowIndexes]; NSArray *files = [[controller arrangedObjects] objectsAtIndexes:selectionIndexes]; if ([tableView tag] == 0) [self stageFiles:files]; else [self unstageFiles:files]; } - (void) rowClicked:(NSCell *)sender { NSTableView *tableView = (NSTableView *)[sender controlView]; if([tableView numberOfSelectedRows] != 1) return; [self tableClicked: tableView]; } - (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pboard { // Copy the row numbers to the pasteboard. [pboard declareTypes:[NSArray arrayWithObjects:FileChangesTableViewType, NSFilenamesPboardType, nil] owner:self]; // Internal, for dragging from one tableview to the other NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; [pboard setData:data forType:FileChangesTableViewType]; // External, to drag them to for example XCode or Textmate NSArrayController *controller = [tv tag] == 0 ? unstagedFilesController : stagedFilesController; NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes]; NSString *workingDirectory = [commitController.repository workingDirectory]; NSMutableArray *filenames = [NSMutableArray arrayWithCapacity:[rowIndexes count]]; for (PBChangedFile *file in files) [filenames addObject:[workingDirectory stringByAppendingPathComponent:[file path]]]; [pboard setPropertyList:filenames forType:NSFilenamesPboardType]; return YES; } - (NSDragOperation)tableView:(NSTableView*)tableView validateDrop:(id )info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)operation { if ([info draggingSource] == tableView) return NSDragOperationNone; [tableView setDropRow:-1 dropOperation:NSTableViewDropOn]; return NSDragOperationCopy; } - (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id )info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation { NSPasteboard* pboard = [info draggingPasteboard]; NSData* rowData = [pboard dataForType:FileChangesTableViewType]; NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData]; NSArrayController *controller = [aTableView tag] == 0 ? stagedFilesController : unstagedFilesController; NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes]; if ([aTableView tag] == 0) [self unstageFiles:files]; else [self stageFiles:files]; return YES; } - (NSString *) contextParameter { return [[NSString alloc] initWithFormat:@"-U%i", contextSize]; } # pragma mark WebKit Accessibility + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector { return NO; } #pragma mark Private Methods - (void)stopTrackingIndex { [stagedFilesController setAutomaticallyRearrangesObjects:NO]; [unstagedFilesController setAutomaticallyRearrangesObjects:NO]; } - (void)resumeTrackingIndex { [stagedFilesController setAutomaticallyRearrangesObjects:YES]; [unstagedFilesController setAutomaticallyRearrangesObjects:YES]; [stagedFilesController rearrangeObjects]; [unstagedFilesController rearrangeObjects]; } @end