// // PBGitCommitController.m // GitX // // Created by Pieter de Bie on 19-09-08. // Copyright 2008 __MyCompanyName__. All rights reserved. // #import "PBGitCommitController.h" #import "NSFileHandleExt.h" #import "PBChangedFile.h" @implementation PBGitCommitController @synthesize files, status, busy, amend; - (void)awakeFromNib { self.files = [NSMutableArray array]; [super awakeFromNib]; [self refresh:self]; [commitMessageView setTypingAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Monaco" size:12.0] forKey:NSFontAttributeName]]; [unstagedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasUnstagedChanges == 1"]]; [cachedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasCachedChanges == 1"]]; [unstagedFilesController setSortDescriptors:[NSArray arrayWithObjects: [[NSSortDescriptor alloc] initWithKey:@"status" ascending:false], [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true], nil]]; [cachedFilesController setSortDescriptors:[NSArray arrayWithObject: [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true]]]; } - (void) removeView { [webController closeView]; [super finalize]; } - (void) setAmend:(BOOL)newAmend { if (newAmend == amend) return; amend = newAmend; if (amend && [[commitMessageView string] length] <= 3) commitMessageView.string = [repository outputForCommand:@"log -1 --pretty=format:%s%n%n%b HEAD"]; [self refresh:self]; } - (NSArray *) linesFromNotification:(NSNotification *)notification { NSDictionary *userInfo = [notification userInfo]; NSData *data = [userInfo valueForKey:NSFileHandleNotificationDataItem]; if (!data) return NULL; NSString* string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if (!string) return NULL; // Strip trailing newline if ([string hasSuffix:@"\n"]) string = [string substringToIndex:[string length]-1]; NSArray *lines = [string componentsSeparatedByString:@"\0"]; return lines; } - (NSString *) parentTree { NSString *parent = amend ? @"HEAD^" : @"HEAD"; if (![repository parseReference:parent]) // We don't have a head ref. Return the empty tree. return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904"; return parent; } - (void) refresh:(id) sender { for (PBChangedFile *file in files) file.shouldBeDeleted = YES; self.status = @"Refreshing index…"; if (![repository workingDirectory]) { //if ([[repository outputForCommand:@"rev-parse --is-inside-work-tree"] isEqualToString:@"false"]) { return; } self.busy++; [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]]; self.busy--; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self]; // Other files NSArray *arguments = [NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil]; NSFileHandle *handle = [repository handleInWorkDirForArguments:arguments]; [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; self.busy++; [handle readToEndOfFileInBackgroundAndNotify]; // Unstaged files handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]]; [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; self.busy++; [handle readToEndOfFileInBackgroundAndNotify]; // Cached files handle = [repository handleInWorkDirForArguments:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]]; [nc addObserver:self selector:@selector(readCachedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; self.busy++; [handle readToEndOfFileInBackgroundAndNotify]; } - (void) updateView { [self refresh:nil]; } - (void) doneProcessingIndex { [self willChangeValueForKey:@"files"]; if (!--self.busy) { self.status = @"Ready"; NSArray *filesToBeDeleted = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"shouldBeDeleted == 1"]]; for (PBChangedFile *file in filesToBeDeleted) { NSLog(@"Deleting file: %@", [file path]); [files removeObject:file]; } } [self didChangeValueForKey:@"files"]; } - (void) readOtherFiles:(NSNotification *)notification; { NSArray *lines = [self linesFromNotification:notification]; for (NSString *line in lines) { if ([line length] == 0) continue; BOOL added = NO; // Check if file is already in our index for (PBChangedFile *file in files) { if ([[file path] isEqualToString:line]) { file.shouldBeDeleted = NO; added = YES; file.status = NEW; file.hasCachedChanges = NO; file.hasUnstagedChanges = YES; continue; } } if (added) continue; // File does not exist yet, so add it PBChangedFile *file =[[PBChangedFile alloc] initWithPath:line]; file.status = NEW; file.hasCachedChanges = NO; file.hasUnstagedChanges = YES; [files addObject: file]; } [self doneProcessingIndex]; } - (void) addFilesFromLines:(NSArray *)lines cached:(BOOL) cached { NSArray *fileStatus; int even = 0; for (NSString *line in lines) { if (!even) { even = 1; fileStatus = [line componentsSeparatedByString:@" "]; continue; } even = 0; NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1]; NSString *sha = [fileStatus objectAtIndex:2]; BOOL isNew = YES; // If the file is already added, we shouldn't add it again // but rather update it to incorporate our changes for (PBChangedFile *file in files) { if ([file.path isEqualToString:line]) { file.shouldBeDeleted = NO; if (cached) { file.commitBlobSHA = sha; file.commitBlobMode = mode; file.hasCachedChanges = YES; } else file.hasUnstagedChanges = YES; isNew = NO; break; } } if (!isNew) continue; PBChangedFile *file = [[PBChangedFile alloc] initWithPath:line]; if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"]) file.status = DELETED; else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"]) file.status = NEW; else file.status = MODIFIED; file.commitBlobSHA = sha; file.commitBlobMode = mode; file.hasCachedChanges = cached; file.hasUnstagedChanges = !cached; [files addObject: file]; } } - (void) readUnstagedFiles:(NSNotification *)notification { NSArray *lines = [self linesFromNotification:notification]; [self addFilesFromLines:lines cached:NO]; [self doneProcessingIndex]; } - (void) readCachedFiles:(NSNotification *)notification { NSArray *lines = [self linesFromNotification:notification]; [self addFilesFromLines:lines cached:YES]; [self doneProcessingIndex]; } - (void) commitFailedBecause:(NSString *)reason { self.busy--; self.status = [@"Commit failed: " stringByAppendingString:reason]; [[NSAlert alertWithMessageText:@"Commit failed" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:reason] runModal]; return; } - (IBAction) commit:(id) sender { if ([[cachedFilesController arrangedObjects] count] == 0) { [[NSAlert alertWithMessageText:@"No changes to commit" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"You must first stage some changes before committing"] runModal]; return; } NSString *commitMessage = [commitMessageView string]; if ([commitMessage length] < 3) { [[NSAlert alertWithMessageText:@"Commitmessage missing" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"Please enter a commit message before committing"] runModal]; return; } [cachedFilesController setSelectionIndexes:[NSIndexSet indexSet]]; [unstagedFilesController setSelectionIndexes:[NSIndexSet indexSet]]; NSString *commitSubject; NSRange newLine = [commitMessage rangeOfString:@"\n"]; if (newLine.location == NSNotFound) commitSubject = commitMessage; else commitSubject = [commitMessage substringToIndex:newLine.location]; commitSubject = [@"commit: " stringByAppendingString:commitSubject]; self.busy++; self.status = @"Creating tree.."; NSString *tree = [repository outputForCommand:@"write-tree"]; if ([tree length] != 40) return [self commitFailedBecause:@"Could not create a tree"]; int ret; NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil]; NSString *parent = amend ? @"HEAD^" : @"HEAD"; if ([repository parseReference:parent]) { [arguments addObject:@"-p"]; [arguments addObject:parent]; } NSString *commit = [repository outputForArguments:arguments inputString:commitMessage retValue: &ret]; if (ret || [commit length] != 40) return [self commitFailedBecause:@"Could not create a commit object"]; [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil] retValue: &ret]; if (ret) return [self commitFailedBecause:@"Could not update HEAD"]; [webController setStateMessage:[NSString stringWithFormat:@"Successfully created commit %@", commit]]; repository.hasChanged = YES; self.busy--; [commitMessageView setString:@""]; amend = NO; [self refresh:self]; self.amend = NO; } - (void) stageHunk:(NSString *)hunk reverse:(BOOL)reverse { NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", @"--cached", nil]; if (reverse) [array addObject:@"--reverse"]; int ret; NSString *error = [repository outputForArguments:array inputString:hunk retValue: &ret]; if (ret) NSLog(@"Error: %@", error); [self refresh:self]; // TODO: We should do this smarter by checking if the file diff is empty, which is faster. } @end