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/PBGitIndex.h b/PBGitIndex.h new file mode 100644 index 0000000..9fc784e --- /dev/null +++ b/PBGitIndex.h @@ -0,0 +1,52 @@ +// +// PBGitIndex.h +// GitX +// +// Created by Pieter de Bie on 9/12/09. +// Copyright 2009 Pieter de Bie. All rights reserved. +// + +#import + +@class PBGitRepository; + +// 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)commit; + +// Inter-file changes: +//- (void)stageFiles:(NSArray *)files; +//- (void)unstageFiles:(NSArray *)files; + +// Intra-file changes +//- (void)applyPatch:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse; + +@end diff --git a/PBGitIndex.m b/PBGitIndex.m new file mode 100644 index 0000000..29e9ff8 --- /dev/null +++ b/PBGitIndex.m @@ -0,0 +1,345 @@ +// +// 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" + +@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; + +@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]; +} + +- (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; +} + +@end + +@implementation PBGitIndex (IndexRefreshMethods) + +- (void)indexRefreshFinished:(NSNotification *)notification +{ + if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue]) + { + // TODO: send updatefailed notification? + return; + } + + // 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 used to add the file to the index + // FIXME: request the real file mode + 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 +{ + // TODO: Stop tracking files + // 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; + } + } + // TODO: Finish tracking files + + // 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: Return 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) + 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"]; + } + + // TODO: Sent index refresh finished operation +} + +@end