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 @implementation PBGitIndexController
17 @synthesize contextSize;
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];
43 [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
44 inputString:input retValue:&ret];
48 NSLog(@"Error when updating index. Retvalue: %i", ret);
52 [self stopTrackingIndex];
53 for (PBChangedFile *file in files)
55 file.hasUnstagedChanges = NO;
56 file.hasStagedChanges = YES;
58 [self resumeTrackingIndex];
61 - (void) unstageFiles:(NSArray *)files
63 NSMutableString *input = [NSMutableString string];
65 for (PBChangedFile *file in files) {
66 [input appendString:[file indexInfo]];
70 [commitController.repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
71 inputString:input retValue:&ret];
75 NSLog(@"Error when updating index. Retvalue: %i", ret);
79 [self stopTrackingIndex];
80 for (PBChangedFile *file in files)
82 file.hasUnstagedChanges = YES;
83 file.hasStagedChanges = NO;
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];
97 NSString *filesAsString = [fileList componentsJoinedByString:@"\n"];
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];
109 ignoreFile = [NSMutableString stringWithContentsOfFile:gitIgnoreName usedEncoding:&enc error:&error];
111 [[commitController.repository windowController] showErrorSheet:error];
114 // Add a newline if not yet present
115 if ([ignoreFile characterAtIndex:([ignoreFile length] - 1)] != '\n')
116 [ignoreFile appendString:@"\n"];
117 [ignoreFile appendString:filesAsString];
120 [ignoreFile writeToFile:gitIgnoreName atomically:YES encoding:enc error:&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
152 return [commitController.repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", [self contextParameter], @"--", file.path, nil]];
155 - (void)discardChangesForFiles:(NSArray *)files force:(BOOL)force
158 int ret = [[NSAlert alertWithMessageText:@"Discard changes"
160 alternateButton:@"Cancel"
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)
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];
172 [commitController.repository outputForArguments:arguments inputString:input retValue:&ret];
174 [[commitController.repository windowController] showMessageSheet:@"Discarding changes failed" infoText:[NSString stringWithFormat:@"Discarding changes failed with error code %i", ret]];
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) {
193 - (NSMenu *) menuForTable:(NSTableView *)table
195 NSMenu *menu = [[NSMenu alloc] init];
196 id controller = [table tag] == 0 ? unstagedFilesController : stagedFilesController;
197 NSArray *selectedFiles = [controller selectedObjects];
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];
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];
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];
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];
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];
235 for (PBChangedFile *file in selectedFiles)
236 if (!file.hasUnstagedChanges)
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];
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];
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)
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];
325 [self unstageFiles:files];
328 - (void) rowClicked:(NSCell *)sender
330 NSTableView *tableView = (NSTableView *)[sender controlView];
331 if([tableView numberOfSelectedRows] != 1)
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];
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
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];
387 [self stageFiles:files];
392 - (NSString *) contextParameter
394 return [[NSString alloc] initWithFormat:@"-U%i", contextSize];
397 # pragma mark WebKit Accessibility
399 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
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];