diff --git a/GitX.xcodeproj/project.pbxproj b/GitX.xcodeproj/project.pbxproj index be97512..d091d92 100644 --- a/GitX.xcodeproj/project.pbxproj +++ b/GitX.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ F59116E90E843BCB0072CCB1 /* PBGitCommitController.m in Sources */ = {isa = PBXBuildFile; fileRef = F59116E80E843BCB0072CCB1 /* PBGitCommitController.m */; }; F593DF780E9E636C003A8559 /* PBFileChangesTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = F593DF770E9E636C003A8559 /* PBFileChangesTableView.m */; }; F5945E170E02B0C200706420 /* PBGitRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = F5945E160E02B0C200706420 /* PBGitRepository.m */; }; + F59F1DD5105C4FF300115F88 /* PBGitIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = F59F1DD4105C4FF300115F88 /* PBGitIndex.m */; }; F5AD56790E79B78100EDAAFE /* PBCommitList.m in Sources */ = {isa = PBXBuildFile; fileRef = F5AD56780E79B78100EDAAFE /* PBCommitList.m */; }; F5B721C40E05CF7E00AF29DC /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = F5B721C20E05CF7E00AF29DC /* MainMenu.xib */; }; F5C007750E731B48007B84B2 /* PBGitRef.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C007740E731B48007B84B2 /* PBGitRef.m */; }; @@ -250,6 +251,8 @@ F593DF770E9E636C003A8559 /* PBFileChangesTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBFileChangesTableView.m; sourceTree = ""; }; F5945E150E02B0C200706420 /* PBGitRepository.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBGitRepository.h; sourceTree = ""; }; F5945E160E02B0C200706420 /* PBGitRepository.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBGitRepository.m; sourceTree = ""; }; + F59F1DD3105C4FF300115F88 /* PBGitIndex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBGitIndex.h; sourceTree = ""; }; + F59F1DD4105C4FF300115F88 /* PBGitIndex.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBGitIndex.m; sourceTree = ""; }; F5AD56770E79B78100EDAAFE /* PBCommitList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBCommitList.h; sourceTree = ""; }; F5AD56780E79B78100EDAAFE /* PBCommitList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBCommitList.m; sourceTree = ""; }; F5B721C30E05CF7E00AF29DC /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/MainMenu.xib; sourceTree = ""; }; @@ -467,6 +470,7 @@ F56174540E05887E001DCD79 /* Git */ = { isa = PBXGroup; children = ( + F59F1DD2105C4FDE00115F88 /* Index */, F5E927E30E883D6800056E75 /* Commit */, F5E927E10E883D2E00056E75 /* History */, F5945E150E02B0C200706420 /* PBGitRepository.h */, @@ -549,6 +553,17 @@ name = SpeedTest; sourceTree = ""; }; + F59F1DD2105C4FDE00115F88 /* Index */ = { + isa = PBXGroup; + children = ( + F5E927F60E883E7200056E75 /* PBChangedFile.h */, + F5E927F70E883E7200056E75 /* PBChangedFile.m */, + F59F1DD3105C4FF300115F88 /* PBGitIndex.h */, + F59F1DD4105C4FF300115F88 /* PBGitIndex.m */, + ); + name = Index; + sourceTree = ""; + }; F5B161BB0EAB6E0C005A1DE1 /* Diff */ = { isa = PBXGroup; children = ( @@ -604,8 +619,6 @@ children = ( 93F7857D0EA3ABF100C1F443 /* PBCommitMessageView.h */, 93F7857E0EA3ABF100C1F443 /* PBCommitMessageView.m */, - F5E927F60E883E7200056E75 /* PBChangedFile.h */, - F5E927F70E883E7200056E75 /* PBChangedFile.m */, F593DF760E9E636C003A8559 /* PBFileChangesTableView.h */, F593DF770E9E636C003A8559 /* PBFileChangesTableView.m */, ); @@ -854,6 +867,7 @@ 47DBDBCA0E95016F00671A1E /* PBNSURLPathUserDefaultsTransfomer.m in Sources */, F562C8870FE1766C000EC528 /* NSString_RegEx.m in Sources */, EB2A734A0FEE3F09006601CF /* PBCollapsibleSplitView.m in Sources */, + F59F1DD5105C4FF300115F88 /* PBGitIndex.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PBGitCommitController.h b/PBGitCommitController.h index 0976738..f29f884 100644 --- a/PBGitCommitController.h +++ b/PBGitCommitController.h @@ -9,12 +9,12 @@ #import #import "PBViewController.h" -@class PBGitIndexController; -@class PBIconAndTextCell; -@class PBWebChangesController; +@class PBGitIndexController, PBIconAndTextCell, PBWebChangesController, PBGitIndex; @interface PBGitCommitController : PBViewController { - NSMutableArray *files; + // This might have to transfer over to the PBGitRepository + // object sometime + PBGitIndex *index; IBOutlet NSTextView *commitMessageView; IBOutlet NSArrayController *unstagedFilesController; @@ -24,28 +24,12 @@ IBOutlet PBWebChangesController *webController; NSString *status; - - // We use busy as a count of active processes. - // You can increase it when your process start - // And decrease it after you have finished. - int busy; - BOOL amend; - NSDictionary *amendEnvironment; - + BOOL busy; } -@property (retain) NSMutableArray *files; -@property (copy) NSString *status; -@property (assign) int busy; -@property (assign) BOOL amend; - -- (void) readCachedFiles:(NSNotification *)notification; -- (void) readOtherFiles:(NSNotification *)notification; -- (void) readUnstagedFiles:(NSNotification *)notification; -- (void) stageHunk: (NSString *)hunk reverse:(BOOL)reverse; -- (void)discardHunk:(NSString *)hunk; - -- (NSString *)parentTree; +@property(copy) NSString *status; +@property(readonly) PBGitIndex *index; +@property(assign) BOOL busy; - (IBAction) refresh:(id) sender; - (IBAction) commit:(id) sender; diff --git a/PBGitCommitController.m b/PBGitCommitController.m index 3f156aa..6a77348 100644 --- a/PBGitCommitController.m +++ b/PBGitCommitController.m @@ -10,26 +10,44 @@ #import "NSFileHandleExt.h" #import "PBChangedFile.h" #import "PBWebChangesController.h" -#import "NSString_RegEx.h" -#import "PBGitIndexController.h" +#import "PBGitIndex.h" -@interface PBGitCommitController (PrivateMethods) -- (NSArray *) linesFromNotification:(NSNotification *)notification; -- (void) doneProcessingIndex; -- (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines; -- (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked; -- (void)processHunk:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse; +@interface PBGitCommitController () +- (void)refreshFinished:(NSNotification *)notification; +- (void)commitStatusUpdated:(NSNotification *)notification; +- (void)commitFinished:(NSNotification *)notification; +- (void)commitFailed:(NSNotification *)notification; +- (void)amendCommit:(NSNotification *)notification; +- (void)indexChanged:(NSNotification *)notification; +- (void)indexOperationFailed:(NSNotification *)notification; @end @implementation PBGitCommitController -@synthesize files, status, busy, amend; +@synthesize status, index, busy; + +- (id)initWithRepository:(PBGitRepository *)theRepository superController:(PBGitWindowController *)controller +{ + if (!(self = [super initWithRepository:theRepository superController:controller])) + return nil; + + index = [[PBGitIndex alloc] initWithRepository:theRepository workingDirectory:[NSURL fileURLWithPath:[theRepository workingDirectory]]]; + [index refresh]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshFinished:) name:PBGitIndexFinishedIndexRefresh object:index]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(commitStatusUpdated:) name:PBGitIndexCommitStatus object:index]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(commitFinished:) name:PBGitIndexFinishedCommit object:index]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(commitFailed:) name:PBGitIndexCommitFailed object:index]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(amendCommit:) name:PBGitIndexAmendMessageAvailable object:index]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(indexChanged:) name:PBGitIndexIndexUpdated object:index]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(indexOperationFailed:) name:PBGitIndexOperationFailed object:index]; + + return self; +} - (void)awakeFromNib { - self.files = [NSMutableArray array]; [super awakeFromNib]; - [self refresh:self]; [commitMessageView setTypingAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Monaco" size:12.0] forKey:NSFontAttributeName]]; @@ -41,12 +59,17 @@ [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true], nil]]; [cachedFilesController setSortDescriptors:[NSArray arrayWithObject: [[NSSortDescriptor alloc] initWithKey:@"path" ascending:true]]]; + + [cachedFilesController setAutomaticallyRearrangesObjects:NO]; + [unstagedFilesController setAutomaticallyRearrangesObjects:NO]; } + - (void) removeView { [webController closeView]; [super finalize]; } + - (NSResponder *)firstResponder; { return commitMessageView; @@ -68,106 +91,11 @@ } } -- (void) setAmend:(BOOL)newAmend -{ - if (newAmend == amend) - return; - - amend = newAmend; - amendEnvironment = nil; - - // If we amend, we want to keep the author information for the previous commit - // We do this by reading in the previous commit, and storing the information - // in a dictionary. This dictionary will then later be read by [self commit:] - if (amend) { - NSString *message = [repository outputForCommand:@"cat-file commit HEAD"]; - NSArray *match = [message substringsMatchingRegularExpression:@"\nauthor ([^\n]*) <([^\n>]*)> ([0-9]+[^\n]*)\n" count:3 options:0 ranges:nil error:nil]; - if (match) - amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME", - [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL", - [match objectAtIndex:3], @"GIT_AUTHOR_DATE", - nil]; - - // Replace commit message with the old one if it's less than 3 characters long. - // This is just a random number. - if ([[commitMessageView string] length] <= 3) { - // Find the commit message - NSRange r = [message rangeOfString:@"\n\n"]; - if (r.location != NSNotFound) - message = [message substringFromIndex:r.location + 2]; - - commitMessageView.string = message; - } - } - - [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 { - if (![repository workingDirectory]) - return; - + self.busy = YES; self.status = @"Refreshing index…"; - - // If self.busy reaches 0, all tasks have finished - self.busy = 0; - - // Refresh the index, necessary for the next methods (that's why it's blocking) - // FIXME: Make this non-blocking. This call can be expensive in large repositories - [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]]; - - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - [nc removeObserver:self]; - - // Other files (not tracked, not ignored) - 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]; - - // Staged 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]; + [index refresh]; // Reload refs (in case HEAD changed) [repository reloadRefs]; @@ -178,154 +106,6 @@ [self refresh:nil]; } -// This method is called for each of the three processes from above. -// If all three are finished (self.busy == 0), then we can delete -// all files previously marked as deletable -- (void) doneProcessingIndex -{ - // if we're still busy, do nothing :) - if (--self.busy) - return; - - NSMutableArray *deleteFiles = [NSMutableArray array]; - for (PBChangedFile *file in files) { - if (!file.hasStagedChanges && !file.hasUnstagedChanges) - [deleteFiles addObject:file]; - } - - if ([deleteFiles count]) { - [self willChangeValueForKey:@"files"]; - for (PBChangedFile *file in deleteFiles) - [files removeObject:file]; - [self didChangeValueForKey:@"files"]; - } - self.status = @"Ready"; -} - -- (void) readOtherFiles:(NSNotification *)notification; -{ - NSArray *lines = [self linesFromNotification:notification]; - NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:[lines count]]; - // We fake this files status as good as possible. - NSArray *fileStatus = [NSArray arrayWithObjects:@":000000", @"100644", @"0000000000000000000000000000000000000000", @"0000000000000000000000000000000000000000", @"A", nil]; - for (NSString *path in lines) { - if ([path length] == 0) - continue; - [dictionary setObject:fileStatus forKey:path]; - } - [self addFilesFromDictionary:dictionary staged:NO tracked:NO]; - [self doneProcessingIndex]; -} - -- (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines -{ - NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2]; - - // Fill the dictionary with the new information - NSArray *fileStatus; - BOOL even = FALSE; - for (NSString *line in lines) { - if (!even) { - even = TRUE; - fileStatus = [line componentsSeparatedByString:@" "]; - continue; - } - - even = FALSE; - [dictionary setObject:fileStatus forKey:line]; - } - return dictionary; -} - -- (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked -{ - // Iterate over all existing files - [indexController stopTrackingIndex]; - for (PBChangedFile *file in files) { - NSArray *fileStatus = [dictionary objectForKey:file.path]; - // Object found, this is still a cached / uncached thing - if (fileStatus) { - if (tracked) { - NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1]; - NSString *sha = [fileStatus objectAtIndex:2]; - file.commitBlobSHA = sha; - file.commitBlobMode = mode; - - if (staged) - file.hasStagedChanges = YES; - else - file.hasUnstagedChanges = YES; - } else { - // Untracked file, set status to NEW, only unstaged changes - file.hasStagedChanges = NO; - file.hasUnstagedChanges = YES; - file.status = NEW; - } - [dictionary removeObjectForKey:file.path]; - } else { // Object not found, let's remove it from the changes - if (staged) - file.hasStagedChanges = NO; - else if (tracked && file.status != NEW) // Only remove it if it's not an untracked file. We handle that with the other thing - file.hasUnstagedChanges = NO; - else if (!tracked && file.status == NEW) - file.hasUnstagedChanges = NO; - } - } - [indexController resumeTrackingIndex]; - - // Do new files - if (![[dictionary allKeys] count]) - return; - - [self willChangeValueForKey:@"files"]; - for (NSString *path in [dictionary allKeys]) { - NSArray *fileStatus = [dictionary objectForKey:path]; - - PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path]; - if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"]) - file.status = DELETED; - else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"]) - file.status = NEW; - else - file.status = MODIFIED; - - if (tracked) { - file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1]; - file.commitBlobSHA = [fileStatus objectAtIndex:2]; - } - - file.hasStagedChanges = staged; - file.hasUnstagedChanges = !staged; - - [files addObject: file]; - } - [self didChangeValueForKey:@"files"]; -} - -- (void) readUnstagedFiles:(NSNotification *)notification -{ - NSArray *lines = [self linesFromNotification:notification]; - NSMutableDictionary *dic = [self dictionaryForLines:lines]; - [self addFilesFromDictionary:dic staged:NO tracked:YES]; - [self doneProcessingIndex]; -} - -- (void) readCachedFiles:(NSNotification *)notification -{ - NSArray *lines = [self linesFromNotification:notification]; - NSMutableDictionary *dic = [self dictionaryForLines:lines]; - [self addFilesFromDictionary:dic staged:YES tracked:YES]; - [self doneProcessingIndex]; -} - -- (void) commitFailedBecause:(NSString *)reason -{ - self.busy--; - self.status = [@"Commit failed: " stringByAppendingString:reason]; - [[repository windowController] showMessageSheet:@"Commit failed" infoText:reason]; - return; -} - - (IBAction) commit:(id) sender { if ([[NSFileManager defaultManager] fileExistsAtPath:[repository.fileURL.path stringByAppendingPathComponent:@"MERGE_HEAD"]]) { @@ -347,98 +127,61 @@ [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 = YES; + [commitMessageView setEditable:NO]; - NSString *commitMessageFile; - commitMessageFile = [repository.fileURL.path - stringByAppendingPathComponent:@"COMMIT_EDITMSG"]; + [index commitWithMessage:commitMessage]; +} - [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil]; - self.busy++; - self.status = @"Creating tree.."; - NSString *tree = [repository outputForCommand:@"write-tree"]; - if ([tree length] != 40) - return [self commitFailedBecause:@"Could not create a tree"]; +# pragma mark PBGitIndex Notification handling +- (void)refreshFinished:(NSNotification *)notification +{ + self.busy = NO; + self.status = @"Index refresh finished"; +} - int ret; +- (void)commitStatusUpdated:(NSNotification *)notification +{ + self.status = [[notification userInfo] objectForKey:@"description"]; +} - 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 - byExtendingEnvironment:amendEnvironment - retValue: &ret]; - - if (ret || [commit length] != 40) - return [self commitFailedBecause:@"Could not create a commit object"]; - - if (![repository executeHook:@"pre-commit" output:nil]) - return [self commitFailedBecause:@"Pre-commit hook failed"]; - - if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil]) - return [self commitFailedBecause:@"Commit-msg hook failed"]; - - [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil] - retValue: &ret]; - if (ret) - return [self commitFailedBecause:@"Could not update HEAD"]; - - if (![repository executeHook:@"post-commit" output:nil]) - [webController setStateMessage:[NSString stringWithFormat:@"Post-commit hook failed, however, successfully created commit %@", commit]]; - else - [webController setStateMessage:[NSString stringWithFormat:@"Successfully created commit %@", commit]]; - - repository.hasChanged = YES; - self.busy--; +- (void)commitFinished:(NSNotification *)notification +{ + [commitMessageView setEditable:YES]; [commitMessageView setString:@""]; - amend = NO; - amendEnvironment = nil; - [self refresh:self]; - self.amend = NO; + [webController setStateMessage:[NSString stringWithFormat:[[notification userInfo] objectForKey:@"description"]]]; +} + +- (void)commitFailed:(NSNotification *)notification +{ + self.busy = NO; + NSString *reason = [[notification userInfo] objectForKey:@"description"]; + self.status = [@"Commit failed: " stringByAppendingString:reason]; + [commitMessageView setEditable:YES]; + [[repository windowController] showMessageSheet:@"Commit failed" infoText:reason]; } -- (void) stageHunk:(NSString *)hunk reverse:(BOOL)reverse +- (void)amendCommit:(NSNotification *)notification { - [self processHunk:hunk stage:TRUE reverse:reverse]; + // Replace commit message with the old one if it's less than 3 characters long. + // This is just a random number. + if ([[commitMessageView string] length] > 3) + return; + + NSString *message = [[notification userInfo] objectForKey:@"message"]; + commitMessageView.string = message; } -- (void)discardHunk:(NSString *)hunk +- (void)indexChanged:(NSNotification *)notification { - [self processHunk:hunk stage:FALSE reverse:TRUE]; + [cachedFilesController rearrangeObjects]; + [unstagedFilesController rearrangeObjects]; } -- (void)processHunk:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse +- (void)indexOperationFailed:(NSNotification *)notification { - NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", nil]; - if (stage) - [array addObject:@"--cached"]; - if (reverse) - [array addObject:@"--reverse"]; - - int ret = 1; - NSString *error = [repository outputForArguments:array - inputString:hunk - retValue:&ret]; - - // FIXME: show this error, rather than just logging it - if (ret) - NSLog(@"Error: %@", error); - - // TODO: We should do this smarter by checking if the file diff is empty, which is faster. - [self refresh:self]; + [[repository windowController] showMessageSheet:@"Index operation failed" infoText:[[notification userInfo] objectForKey:@"description"]]; } @end diff --git a/PBGitCommitView.xib b/PBGitCommitView.xib index 643bfa3..5b8021f 100644 --- a/PBGitCommitView.xib +++ b/PBGitCommitView.xib @@ -2,15 +2,14 @@ 1050 - 9J61 + 9L31a 677 - 949.46 + 949.54 353.00 YES + - - YES @@ -127,7 +126,7 @@ YES - + @@ -963,38 +962,6 @@ 139 - - - contentArray: files - - - - - - contentArray: files - contentArray - files - 2 - - - 149 - - - - contentArray: files - - - - - - contentArray: files - contentArray - files - 2 - - - 150 - cachedFilesController @@ -1067,22 +1034,6 @@ 241 - - - value: amend - - - - - - value: amend - value - amend - 2 - - - 252 - webController @@ -1163,22 +1114,6 @@ 264 - - - stagedButtonCell - - - - 265 - - - - unstagedButtonCell - - - - 266 - rowClicked: @@ -1219,6 +1154,54 @@ 280 + + + contentArray: index.indexChanges + + + + + + contentArray: index.indexChanges + contentArray + index.indexChanges + 2 + + + 281 + + + + contentArray: index.indexChanges + + + + + + contentArray: index.indexChanges + contentArray + index.indexChanges + 2 + + + 282 + + + + value: index.amend + + + + + + value: index.amend + value + index.amend + 2 + + + 283 + @@ -1616,7 +1599,7 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilderKit com.apple.InterfaceBuilderKit - {{59, 63}, {852, 432}} + {{428, 510}, {852, 432}} com.apple.InterfaceBuilder.CocoaPlugin @@ -1680,7 +1663,7 @@ - 280 + 283 @@ -1764,20 +1747,16 @@ YES commitController - stagedButtonCell stagedFilesController stagedTable - unstagedButtonCell unstagedFilesController unstagedTable YES PBGitCommitController - PBIconAndTextCell NSArrayController NSTableView - PBIconAndTextCell NSArrayController NSTableView diff --git a/PBGitIndex.h b/PBGitIndex.h new file mode 100644 index 0000000..7ca1db7 --- /dev/null +++ b/PBGitIndex.h @@ -0,0 +1,80 @@ +// +// PBGitIndex.h +// GitX +// +// Created by Pieter de Bie on 9/12/09. +// Copyright 2009 Pieter de Bie. All rights reserved. +// + +#import + +@class PBGitRepository; +@class PBChangedFile; + +/* + * Notifications this class will send + */ + +// Refreshing index +extern NSString *PBGitIndexIndexRefreshStatus; +extern NSString *PBGitIndexIndexRefreshFailed; +extern NSString *PBGitIndexFinishedIndexRefresh; + +// The "indexChanges" array has changed +extern NSString *PBGitIndexIndexUpdated; + +// Committing files +extern NSString *PBGitIndexCommitStatus; +extern NSString *PBGitIndexCommitFailed; +extern NSString *PBGitIndexFinishedCommit; + +// Changing to amend +extern NSString *PBGitIndexAmendMessageAvailable; + +// This is for general operations, like applying a patch +extern NSString *PBGitIndexOperationFailed; + + + +// Represents a git index for a given work tree. +// As a single git repository can have multiple trees, +// the tree has to be given explicitly, even though +// multiple trees is not yet supported in GitX +@interface PBGitIndex : NSObject { + +@private + PBGitRepository *repository; + NSURL *workingDirectory; + NSMutableArray *files; + + NSUInteger refreshStatus; + NSDictionary *amendEnvironment; + BOOL amend; +} + +// Whether we want the changes for amending, +// or for +@property BOOL amend; + +- (id)initWithRepository:(PBGitRepository *)repository workingDirectory:(NSURL *)workingDirectory; + +// A list of PBChangedFile's with differences between the work tree and the index +// This method is KVO-aware, so changes when any of the index-modifying methods are called +// (including -refresh) +- (NSArray *)indexChanges; + +// Refresh the index +- (void)refresh; + +- (void)commitWithMessage:(NSString *)commitMessage; + +// Inter-file changes: +- (BOOL)stageFiles:(NSArray *)stageFiles; +- (BOOL)unstageFiles:(NSArray *)unstageFiles; +- (void)discardChangesForFiles:(NSArray *)discardFiles; + +// Intra-file changes +- (BOOL)applyPatch:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse; +- (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context; + +@end diff --git a/PBGitIndex.m b/PBGitIndex.m new file mode 100644 index 0000000..4174d49 --- /dev/null +++ b/PBGitIndex.m @@ -0,0 +1,630 @@ +// +// PBGitIndex.m +// GitX +// +// Created by Pieter de Bie on 9/12/09. +// Copyright 2009 Pieter de Bie. All rights reserved. +// + +#import "PBGitIndex.h" +#import "PBGitRepository.h" +#import "PBGitBinary.h" +#import "PBEasyPipe.h" +#import "NSString_RegEx.h" +#import "PBChangedFile.h" + +NSString *PBGitIndexIndexRefreshStatus = @"PBGitIndexIndexRefreshStatus"; +NSString *PBGitIndexIndexRefreshFailed = @"PBGitIndexIndexRefreshFailed"; +NSString *PBGitIndexFinishedIndexRefresh = @"PBGitIndexFinishedIndexRefresh"; + +NSString *PBGitIndexIndexUpdated = @"GBGitIndexIndexUpdated"; + +NSString *PBGitIndexCommitStatus = @"PBGitIndexCommitStatus"; +NSString *PBGitIndexCommitFailed = @"PBGitIndexCommitFailed"; +NSString *PBGitIndexFinishedCommit = @"PBGitIndexFinishedCommit"; + +NSString *PBGitIndexAmendMessageAvailable = @"PBGitIndexAmendMessageAvailable"; +NSString *PBGitIndexOperationFailed = @"PBGitIndexOperationFailed"; + +@interface PBGitIndex (IndexRefreshMethods) + +- (NSArray *)linesFromNotification:(NSNotification *)notification; +- (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines; +- (void)addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked; + +- (void)indexStepComplete; + +- (void)indexRefreshFinished:(NSNotification *)notification; +- (void)readOtherFiles:(NSNotification *)notification; +- (void)readUnstagedFiles:(NSNotification *)notification; +- (void)readStagedFiles:(NSNotification *)notification; + +@end + +@interface PBGitIndex () + +// Returns the tree to compare the index to, based +// on whether amend is set or not. +- (NSString *) parentTree; +- (void)postCommitUpdate:(NSString *)update; +- (void)postCommitFailure:(NSString *)reason; +- (void)postIndexChange; +- (void)postOperationFailed:(NSString *)description; +@end + +@implementation PBGitIndex + +@synthesize amend; + +- (id)initWithRepository:(PBGitRepository *)theRepository workingDirectory:(NSURL *)theWorkingDirectory +{ + if (!(self = [super init])) + return nil; + + NSAssert(theWorkingDirectory, @"PBGitIndex requires a working directory"); + NSAssert(theRepository, @"PBGitIndex requires a repository"); + + repository = theRepository; + workingDirectory = theWorkingDirectory; + files = [NSMutableArray array]; + + return self; +} + +- (NSArray *)indexChanges +{ + return files; +} + +- (void)setAmend:(BOOL)newAmend +{ + if (newAmend == amend) + return; + + amend = newAmend; + amendEnvironment = nil; + + [self refresh]; + + if (!newAmend) + return; + + // If we amend, we want to keep the author information for the previous commit + // We do this by reading in the previous commit, and storing the information + // in a dictionary. This dictionary will then later be read by [self commit:] + NSString *message = [repository outputForCommand:@"cat-file commit HEAD"]; + NSArray *match = [message substringsMatchingRegularExpression:@"\nauthor ([^\n]*) <([^\n>]*)> ([0-9]+[^\n]*)\n" count:3 options:0 ranges:nil error:nil]; + if (match) + amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME", + [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL", + [match objectAtIndex:3], @"GIT_AUTHOR_DATE", + nil]; + + // Find the commit message + NSRange r = [message rangeOfString:@"\n\n"]; + if (r.location != NSNotFound) { + NSString *commitMessage = [message substringFromIndex:r.location + 2]; + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexAmendMessageAvailable + object: self + userInfo:[NSDictionary dictionaryWithObject:commitMessage forKey:@"message"]]; + } + +} + +- (void)refresh +{ + // If we were already refreshing the index, we don't want + // double notifications. As we can't stop the tasks anymore, + // just cancel the notifications + refreshStatus = 0; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self]; + + // Ask Git to refresh the index + NSFileHandle *updateHandle = [PBEasyPipe handleForCommand:[PBGitBinary path] + withArgs:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil] + inDir:[workingDirectory path]]; + + [nc addObserver:self + selector:@selector(indexRefreshFinished:) + name:NSFileHandleReadToEndOfFileCompletionNotification + object:updateHandle]; + [updateHandle readToEndOfFileInBackgroundAndNotify]; + +} + +- (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; +} + +// TODO: make Asynchronous +- (void)commitWithMessage:(NSString *)commitMessage +{ + NSMutableString *commitSubject = [@"commit: " mutableCopy]; + NSRange newLine = [commitMessage rangeOfString:@"\n"]; + if (newLine.location == NSNotFound) + [commitSubject appendString:commitMessage]; + else + [commitSubject appendString:[commitMessage substringToIndex:newLine.location]]; + + NSString *commitMessageFile; + commitMessageFile = [repository.fileURL.path stringByAppendingPathComponent:@"COMMIT_EDITMSG"]; + + [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil]; + + + [self postCommitUpdate:@"Creating tree"]; + NSString *tree = [repository outputForCommand:@"write-tree"]; + if ([tree length] != 40) + return [self postCommitFailure:@"Creating tree failed"]; + + + NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil]; + NSString *parent = amend ? @"HEAD^" : @"HEAD"; + if ([repository parseReference:parent]) { + [arguments addObject:@"-p"]; + [arguments addObject:parent]; + } + + [self postCommitUpdate:@"Creating commit"]; + int ret = 1; + NSString *commit = [repository outputForArguments:arguments + inputString:commitMessage + byExtendingEnvironment:amendEnvironment + retValue: &ret]; + + if (ret || [commit length] != 40) + return [self postCommitFailure:@"Could not create a commit object"]; + + [self postCommitUpdate:@"Running hooks"]; + if (![repository executeHook:@"pre-commit" output:nil]) + return [self postCommitFailure:@"Pre-commit hook failed"]; + + if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil]) + return [self postCommitFailure:@"Commit-msg hook failed"]; + + [self postCommitUpdate:@"Updating HEAD"]; + [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil] + retValue: &ret]; + if (ret) + return [self postCommitFailure:@"Could not update HEAD"]; + + [self postCommitUpdate:@"Running post-commit hook"]; + + BOOL success = [repository executeHook:@"post-commit" output:nil]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithBool:success] forKey:@"success"]; + NSString *description; + if (success) + description = [NSString stringWithFormat:@"Successfull created commit %@", commit]; + else + description = [NSString stringWithFormat:@"Post-commit hook failed, but successfully created commit %@", commit]; + + [userInfo setObject:description forKey:@"description"]; + [userInfo setObject:commit forKey:@"sha"]; + + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedCommit + object:self + userInfo:userInfo]; + if (!success) + return; + + repository.hasChanged = YES; + + amendEnvironment = nil; + if (amend) + self.amend = NO; + else + [self refresh]; + +} + +- (void)postCommitUpdate:(NSString *)update +{ + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitStatus + object:self + userInfo:[NSDictionary dictionaryWithObject:update forKey:@"description"]]; +} + +- (void)postCommitFailure:(NSString *)reason +{ + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitFailed + object:self + userInfo:[NSDictionary dictionaryWithObject:reason forKey:@"description"]]; +} + +- (void)postOperationFailed:(NSString *)description +{ + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexOperationFailed + object:self + userInfo:[NSDictionary dictionaryWithObject:description forKey:@"description"]]; +} + +- (BOOL)stageFiles:(NSArray *)stageFiles +{ + // Input string for update-index + // This will be a list of filenames that + // should be updated. It's similar to + // "git add -- + NSMutableString *input = [NSMutableString string]; + + for (PBChangedFile *file in stageFiles) { + [input appendFormat:@"%@\0", file.path]; + } + + int ret = 1; + [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil] + inputString:input + retValue:&ret]; + + if (ret) { + [self postOperationFailed:[NSString stringWithFormat:@"Error in staging files. Return value: %i", ret]]; + return NO; + } + + for (PBChangedFile *file in stageFiles) + { + file.hasUnstagedChanges = NO; + file.hasStagedChanges = YES; + } + + [self postIndexChange]; + return YES; +} + +// TODO: Refactor with above. What's a better name for this? +- (BOOL)unstageFiles:(NSArray *)unstageFiles +{ + NSMutableString *input = [NSMutableString string]; + + for (PBChangedFile *file in unstageFiles) { + [input appendString:[file indexInfo]]; + } + + int ret = 1; + [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil] + inputString:input + retValue:&ret]; + + if (ret) + { + [self postOperationFailed:[NSString stringWithFormat:@"Error in unstaging files. Return value: %i", ret]]; + return NO; + } + + for (PBChangedFile *file in unstageFiles) + { + file.hasUnstagedChanges = YES; + file.hasStagedChanges = NO; + } + + [self postIndexChange]; + return YES; +} + +- (void)discardChangesForFiles:(NSArray *)discardFiles +{ + NSArray *paths = [discardFiles valueForKey:@"path"]; + NSString *input = [paths componentsJoinedByString:@"\0"]; + + NSArray *arguments = [NSArray arrayWithObjects:@"checkout-index", @"--index", @"--quiet", @"--force", @"-z", @"--stdin", nil]; + + int ret = 1; + [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir:[workingDirectory path] inputString:input retValue:&ret]; + + if (ret) { + [self postOperationFailed:[NSString stringWithFormat:@"Discarding changes failed with return value %i", ret]]; + return; + } + + for (PBChangedFile *file in discardFiles) + file.hasUnstagedChanges = NO; + + [self postIndexChange]; +} + +- (BOOL)applyPatch:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse; +{ + NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", nil]; + if (stage) + [array addObject:@"--cached"]; + if (reverse) + [array addObject:@"--reverse"]; + + int ret = 1; + NSString *error = [repository outputForArguments:array + inputString:hunk + retValue:&ret]; + + if (ret) { + [self postOperationFailed:[NSString stringWithFormat:@"Applying patch failed with return value %i. Error: %@", ret, error]]; + return NO; + } + + // TODO: Try to be smarter about what to refresh + [self refresh]; + return YES; +} + + +- (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context +{ + NSString *parameter = [NSString stringWithFormat:@"-U%u", context]; + if (staged) { + NSString *indexPath = [@":0:" stringByAppendingString:file.path]; + + if (file.status == NEW) + return [repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]]; + + return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", parameter, @"--cached", [self parentTree], @"--", file.path, nil]]; + } + + // unstaged + if (file.status == NEW) { + NSStringEncoding encoding; + NSError *error = nil; + NSString *path = [[repository workingDirectory] stringByAppendingPathComponent:file.path]; + NSString *contents = [NSString stringWithContentsOfFile:path + usedEncoding:&encoding + error:&error]; + if (error) + return nil; + + return contents; + } + + return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", parameter, @"--", file.path, nil]]; +} + +- (void)postIndexChange +{ + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexUpdated + object:self]; +} + +# pragma mark WebKit Accessibility + ++ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector +{ + return NO; +} + +@end + +@implementation PBGitIndex (IndexRefreshMethods) + +- (void)indexRefreshFinished:(NSNotification *)notification +{ + if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue]) + { + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshFailed + object:self + userInfo:[NSDictionary dictionaryWithObject:@"update-index failed" forKey:@"description"]]; + return; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshStatus + object:self + userInfo:[NSDictionary dictionaryWithObject:@"update-index success" forKey:@"description"]]; + + // Now that the index is refreshed, we need to read the information from the index + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + // Other files (not tracked, not ignored) + NSFileHandle *handle = [PBEasyPipe handleForCommand:[PBGitBinary path] + withArgs:[NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil] + inDir:[workingDirectory path]]; + [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; + [handle readToEndOfFileInBackgroundAndNotify]; + refreshStatus++; + + // Unstaged files + handle = [PBEasyPipe handleForCommand:[PBGitBinary path] + withArgs:[NSArray arrayWithObjects:@"diff-files", @"-z", nil] + inDir:[workingDirectory path]]; + [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; + [handle readToEndOfFileInBackgroundAndNotify]; + refreshStatus++; + + // Staged files + handle = [PBEasyPipe handleForCommand:[PBGitBinary path] + withArgs:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil] + inDir:[workingDirectory path]]; + [nc addObserver:self selector:@selector(readStagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; + [handle readToEndOfFileInBackgroundAndNotify]; + refreshStatus++; +} + +- (void)readOtherFiles:(NSNotification *)notification +{ + NSArray *lines = [self linesFromNotification:notification]; + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:[lines count]]; + // Other files are untracked, so we don't have any real index information. Instead, we can just fake it. + // The line below is not used at all, as for these files the commitBlob isn't set + NSArray *fileStatus = [NSArray arrayWithObjects:@":000000", @"100644", @"0000000000000000000000000000000000000000", @"0000000000000000000000000000000000000000", @"A", nil]; + for (NSString *path in lines) { + if ([path length] == 0) + continue; + [dictionary setObject:fileStatus forKey:path]; + } + + [self addFilesFromDictionary:dictionary staged:NO tracked:NO]; + [self indexStepComplete]; +} + +- (void) readStagedFiles:(NSNotification *)notification +{ + NSArray *lines = [self linesFromNotification:notification]; + NSMutableDictionary *dic = [self dictionaryForLines:lines]; + [self addFilesFromDictionary:dic staged:YES tracked:YES]; + [self indexStepComplete]; +} + +- (void) readUnstagedFiles:(NSNotification *)notification +{ + NSArray *lines = [self linesFromNotification:notification]; + NSMutableDictionary *dic = [self dictionaryForLines:lines]; + [self addFilesFromDictionary:dic staged:NO tracked:YES]; + [self indexStepComplete]; +} + +- (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked +{ + // Iterate over all existing files + for (PBChangedFile *file in files) { + NSArray *fileStatus = [dictionary objectForKey:file.path]; + // Object found, this is still a cached / uncached thing + if (fileStatus) { + if (tracked) { + NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1]; + NSString *sha = [fileStatus objectAtIndex:2]; + file.commitBlobSHA = sha; + file.commitBlobMode = mode; + + if (staged) + file.hasStagedChanges = YES; + else + file.hasUnstagedChanges = YES; + } else { + // Untracked file, set status to NEW, only unstaged changes + file.hasStagedChanges = NO; + file.hasUnstagedChanges = YES; + file.status = NEW; + } + + // We handled this file, remove it from the dictionary + [dictionary removeObjectForKey:file.path]; + } else { + // Object not found in the dictionary, so let's reset its appropriate + // change (stage or untracked) if necessary. + + // Staged dictionary, so file does not have staged changes + if (staged) + file.hasStagedChanges = NO; + // Tracked file does not have unstaged changes, file is not new, + // so we can set it to No. (If it would be new, it would not + // be in this dictionary, but in the "other dictionary"). + else if (tracked && file.status != NEW) + file.hasUnstagedChanges = NO; + // Unstaged, untracked dictionary ("Other" files), and file + // is indicated as new (which would be untracked), so let's + // remove it + else if (!tracked && file.status == NEW) + file.hasUnstagedChanges = NO; + } + } + + // Do new files only if necessary + if (![[dictionary allKeys] count]) + return; + + // All entries left in the dictionary haven't been accounted for + // above, so we need to add them to the "files" array + [self willChangeValueForKey:@"indexChanges"]; + for (NSString *path in [dictionary allKeys]) { + NSArray *fileStatus = [dictionary objectForKey:path]; + + PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path]; + if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"]) + file.status = DELETED; + else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"]) + file.status = NEW; + else + file.status = MODIFIED; + + if (tracked) { + file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1]; + file.commitBlobSHA = [fileStatus objectAtIndex:2]; + } + + file.hasStagedChanges = staged; + file.hasUnstagedChanges = !staged; + + [files addObject:file]; + } + [self didChangeValueForKey:@"indexChanges"]; +} + +# pragma mark Utility methods +- (NSArray *)linesFromNotification:(NSNotification *)notification +{ + NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem]; + if (!data) + return [NSArray array]; + + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + // FIXME: throw an error? + if (!string) + return [NSArray array]; + + // Strip trailing null + if ([string hasSuffix:@"\0"]) + string = [string substringToIndex:[string length]-1]; + + if ([string length] == 0) + return [NSArray array]; + + return [string componentsSeparatedByString:@"\0"]; +} + +- (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines +{ + NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2]; + + // Fill the dictionary with the new information. These lines are in the form of: + // :00000 :0644 OTHER INDEX INFORMATION + // Filename + + NSAssert1([lines count] % 2 == 0, @"Lines must have an even number of lines: %@", lines); + + NSEnumerator *enumerator = [lines objectEnumerator]; + NSString *fileStatus; + while (fileStatus = [enumerator nextObject]) { + NSString *fileName = [enumerator nextObject]; + [dictionary setObject:[fileStatus componentsSeparatedByString:@" "] forKey:fileName]; + } + + return dictionary; +} + +// This method is called for each of the three processes from above. +// If all three are finished (self.busy == 0), then we can delete +// all files previously marked as deletable +- (void)indexStepComplete +{ + // if we're still busy, do nothing :) + if (--refreshStatus) { + [self postIndexChange]; + return; + } + + // At this point, all index operations have finished. + // We need to find all files that don't have either + // staged or unstaged files, and delete them + + NSMutableArray *deleteFiles = [NSMutableArray array]; + for (PBChangedFile *file in files) { + if (!file.hasStagedChanges && !file.hasUnstagedChanges) + [deleteFiles addObject:file]; + } + + if ([deleteFiles count]) { + [self willChangeValueForKey:@"indexChanges"]; + for (PBChangedFile *file in deleteFiles) + [files removeObject:file]; + [self didChangeValueForKey:@"indexChanges"]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedIndexRefresh + object:self]; + [self postIndexChange]; + +} + +@end diff --git a/PBGitIndexController.h b/PBGitIndexController.h index 184d670..1a455e6 100644 --- a/PBGitIndexController.h +++ b/PBGitIndexController.h @@ -11,33 +11,15 @@ #import "PBChangedFile.h" @interface PBGitIndexController : NSObject { - int contextSize; - IBOutlet NSArrayController *stagedFilesController, *unstagedFilesController; IBOutlet PBGitCommitController *commitController; - IBOutlet PBIconAndTextCell* unstagedButtonCell; - IBOutlet PBIconAndTextCell* stagedButtonCell; - IBOutlet NSTableView *unstagedTable; IBOutlet NSTableView *stagedTable; } -@property (assign) int contextSize; - -- (NSString *) contextParameter; - -- (void) stageFiles:(NSArray *)files; -- (void) unstageFiles:(NSArray *)files; - - (IBAction) rowClicked:(NSCell *) sender; - (IBAction) tableClicked:(NSTableView *)tableView; -- (NSString *) stagedChangesForFile:(PBChangedFile *)file; -- (NSString *) unstagedChangesForFile:(PBChangedFile *)file; - -- (void)stopTrackingIndex; -- (void)resumeTrackingIndex; - - (NSMenu *) menuForTable:(NSTableView *)table; @end diff --git a/PBGitIndexController.m b/PBGitIndexController.m index 6645ca1..5d2a949 100644 --- a/PBGitIndexController.m +++ b/PBGitIndexController.m @@ -9,17 +9,18 @@ #import "PBGitIndexController.h" #import "PBChangedFile.h" #import "PBGitRepository.h" +#import "PBGitIndex.h" #define FileChangesTableViewType @"GitFileChangedType" -@implementation PBGitIndexController +@interface PBGitIndexController () +- (void)discardChangesForFiles:(NSArray *)files force:(BOOL)force; +@end -@synthesize contextSize; +@implementation PBGitIndexController - (void)awakeFromNib { - contextSize = 3; - [unstagedTable setDoubleAction:@selector(tableClicked:)]; [stagedTable setDoubleAction:@selector(tableClicked:)]; @@ -28,64 +29,10 @@ [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 +// FIXME: Find a proper place for this method -- this is not it. +- (void)ignoreFiles:(NSArray *)files { // Build output string NSMutableArray *fileList = [NSMutableArray array]; @@ -122,63 +69,6 @@ [[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 { @@ -255,12 +145,12 @@ - (void) stageFilesAction:(id) sender { - [self stageFiles:[sender representedObject]]; + [commitController.index stageFiles:[sender representedObject]]; } - (void) unstageFilesAction:(id) sender { - [self unstageFiles:[sender representedObject]]; + [commitController.index unstageFiles:[sender representedObject]]; } - (void) openFilesAction:(id) sender @@ -274,10 +164,11 @@ - (void) ignoreFilesAction:(id) sender { NSArray *selectedFiles = [sender representedObject]; - if ([selectedFiles count] > 0) { - [self ignoreFiles:selectedFiles]; - } - [commitController refresh:NULL]; + if ([selectedFiles count] == 0) + return; + + [self ignoreFiles:selectedFiles]; + [commitController.index refresh]; } - (void)discardFilesAction:(id) sender @@ -305,6 +196,20 @@ [ws selectFile: path inFileViewerRootedAtPath: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; + } + + [commitController.index discardChangesForFiles:files]; +} # pragma mark TableView icon delegate - (void)tableView:(NSTableView*)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex @@ -320,9 +225,9 @@ NSIndexSet *selectionIndexes = [tableView selectedRowIndexes]; NSArray *files = [[controller arrangedObjects] objectsAtIndexes:selectionIndexes]; if ([tableView tag] == 0) - [self stageFiles:files]; + [commitController.index stageFiles:files]; else - [self unstageFiles:files]; + [commitController.index unstageFiles:files]; } - (void) rowClicked:(NSCell *)sender @@ -382,36 +287,11 @@ writeRowsWithIndexes:(NSIndexSet *)rowIndexes NSArray *files = [[controller arrangedObjects] objectsAtIndexes:rowIndexes]; if ([aTableView tag] == 0) - [self unstageFiles:files]; + [commitController.index unstageFiles:files]; else - [self stageFiles:files]; + [commitController.index 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 diff --git a/PBWebChangesController.h b/PBWebChangesController.h index 0b0ec13..069f6fc 100644 --- a/PBWebChangesController.h +++ b/PBWebChangesController.h @@ -27,5 +27,4 @@ - (void) setStateMessage:(NSString *)state; - (void) showMultiple:(NSArray *)files; -- (void) setContextSize:(int)size; @end diff --git a/PBWebChangesController.m b/PBWebChangesController.m index d24a8af..933fecc 100644 --- a/PBWebChangesController.m +++ b/PBWebChangesController.m @@ -8,6 +8,7 @@ #import "PBWebChangesController.h" #import "PBGitIndexController.h" +#import "PBGitIndex.h" @implementation PBWebChangesController @@ -25,15 +26,10 @@ - (void) didLoad { - [[self script] setValue:indexController forKey:@"IndexController"]; + [[self script] setValue:controller.index forKey:@"Index"]; [self refresh]; } -- (BOOL) amend -{ - return controller.amend; -} - - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change @@ -81,9 +77,11 @@ [NSNumber numberWithBool:selectedFileIsCached], nil]]; } -- (void) stageHunk:(NSString *)hunk reverse:(BOOL)reverse +- (void)stageHunk:(NSString *)hunk reverse:(BOOL)reverse { - [controller stageHunk: hunk reverse:reverse]; + [controller.index applyPatch:hunk stage:YES reverse:reverse]; + // FIXME: Don't need a hard refresh + [self refresh]; } @@ -99,7 +97,7 @@ } if (ret == NSAlertDefaultReturn) { - [controller discardHunk:hunk]; + [controller.index applyPatch:hunk stage:NO reverse:YES]; [self refresh]; } } @@ -110,12 +108,4 @@ [script callWebScriptMethod:@"setState" withArguments: [NSArray arrayWithObject:state]]; } -- (void) setContextSize:(int)size -{ - if (size == indexController.contextSize) - return; - - indexController.contextSize = size; - [self refresh]; -} @end diff --git a/html/views/commit/commit.js b/html/views/commit/commit.js index df0cb42..cdec998 100644 --- a/html/views/commit/commit.js +++ b/html/views/commit/commit.js @@ -1,11 +1,13 @@ /* Commit: Interface for selecting, staging, discarding, and unstaging hunks, individual lines, or ranges of lines. */ +var contextLines = 5; + var showNewFile = function(file) { setTitle("New file: " + file.path); - var contents = IndexController.unstagedChangesForFile_(file); + var contents = Index.diffForFile_staged_contextLines_(file, false, contextLines); if (!contents) { notify("Can not display changes (Binary file?)", -1); diff.innerHTML = ""; @@ -49,23 +51,16 @@ var showFileChanges = function(file, cached) { hideState(); $("contextSize").oninput = function(element) { - Controller.setContextSize_($("contextSize").value); + contextSize = $("contextSize").value; } if (file.status == 0) // New file? return showNewFile(file); - var changes; - if (cached) { - setTitle("Staged changes for " + file.path); - displayContext(); - changes = IndexController.stagedChangesForFile_(file); - } - else { - setTitle("Unstaged changes for " + file.path); - displayContext(); - changes = IndexController.unstagedChangesForFile_(file); - } + setTitle((cached ? "Staged": "Unstaged") + " changes for" + file.path); + displayContext(); + var changes = Index.diffForFile_staged_contextLines_(file, cached, contextLines); + if (changes == "") { notify("This file has no more changes", 1);