Merge branch 'pu/pb/index_refactor'

* pu/pb/index_refactor: (24 commits)
  PBGitIndex: post notifications when index stuff fails
  GitIndexController: reorder methods a bit, remove unnecessary stuff
  PBGitIndex: Migrate discardChanges to the GitIndex
  CommitController: Make commit message editable after commit failed
  GitIndex: Fix a few comments
  GitIndex: explicitly tell when stuff is updated
  Remove cruft
  Show previous commit message when amending
  CommitController: Empty commit title when commit is successful
  CommitView: Remove cruft
  Add failed commit notifications
  GitIndex: add commit notifications
  CommitController: Add status messages for index operations
  GitIndex: Add a few notifications
  CommitView: Migrate patch apply stuff to GitIndex
  GitIndex: Add support for applying patches
  CommitController: Replace commit method with the one from GitIndex
  GitIndex: add basic commit method
  GitIndexController: Migrate stageFiles functions to GitIndex
  GitIndex: Add methods to stage and unstage files
  ...

Conflicts:
	PBGitCommitController.m
	PBGitIndexController.h
	PBGitIndexController.m
This commit is contained in:
Pieter de Bie
2009-09-17 23:47:43 +02:00
11 changed files with 911 additions and 635 deletions
+16 -2
View File
@@ -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 = "<group>"; };
F5945E150E02B0C200706420 /* PBGitRepository.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBGitRepository.h; sourceTree = "<group>"; };
F5945E160E02B0C200706420 /* PBGitRepository.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBGitRepository.m; sourceTree = "<group>"; };
F59F1DD3105C4FF300115F88 /* PBGitIndex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBGitIndex.h; sourceTree = "<group>"; };
F59F1DD4105C4FF300115F88 /* PBGitIndex.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBGitIndex.m; sourceTree = "<group>"; };
F5AD56770E79B78100EDAAFE /* PBCommitList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBCommitList.h; sourceTree = "<group>"; };
F5AD56780E79B78100EDAAFE /* PBCommitList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBCommitList.m; sourceTree = "<group>"; };
F5B721C30E05CF7E00AF29DC /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -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 = "<group>";
};
F59F1DD2105C4FDE00115F88 /* Index */ = {
isa = PBXGroup;
children = (
F5E927F60E883E7200056E75 /* PBChangedFile.h */,
F5E927F70E883E7200056E75 /* PBChangedFile.m */,
F59F1DD3105C4FF300115F88 /* PBGitIndex.h */,
F59F1DD4105C4FF300115F88 /* PBGitIndex.m */,
);
name = Index;
sourceTree = "<group>";
};
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;
};
+8 -24
View File
@@ -9,12 +9,12 @@
#import <Cocoa/Cocoa.h>
#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;
+76 -333
View File
@@ -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
+54 -75
View File
@@ -2,15 +2,14 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.03">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">9J61</string>
<string key="IBDocument.SystemVersion">9L31a</string>
<string key="IBDocument.InterfaceBuilderVersion">677</string>
<string key="IBDocument.AppKitVersion">949.46</string>
<string key="IBDocument.AppKitVersion">949.54</string>
<string key="IBDocument.HIToolboxVersion">353.00</string>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="207"/>
<integer value="225"/>
<integer value="113"/>
<integer value="1" id="9"/>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -127,7 +126,7 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12" id="777559147"/>
<reference ref="777559147"/>
<reference ref="9"/>
<integer value="1" id="9"/>
</object>
</object>
</object>
@@ -963,38 +962,6 @@
</object>
<int key="connectionID">139</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">contentArray: files</string>
<reference key="source" ref="667905213"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="667905213"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">contentArray: files</string>
<string key="NSBinding">contentArray</string>
<string key="NSKeyPath">files</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">149</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">contentArray: files</string>
<reference key="source" ref="128809524"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="128809524"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">contentArray: files</string>
<string key="NSBinding">contentArray</string>
<string key="NSKeyPath">files</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">150</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">cachedFilesController</string>
@@ -1067,22 +1034,6 @@
</object>
<int key="connectionID">241</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">value: amend</string>
<reference key="source" ref="18874447"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="18874447"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">value: amend</string>
<string key="NSBinding">value</string>
<string key="NSKeyPath">amend</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">252</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webController</string>
@@ -1163,22 +1114,6 @@
</object>
<int key="connectionID">264</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">stagedButtonCell</string>
<reference key="source" ref="446885874"/>
<reference key="destination" ref="39450212"/>
</object>
<int key="connectionID">265</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">unstagedButtonCell</string>
<reference key="source" ref="446885874"/>
<reference key="destination" ref="45690317"/>
</object>
<int key="connectionID">266</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">rowClicked:</string>
@@ -1219,6 +1154,54 @@
</object>
<int key="connectionID">280</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">contentArray: index.indexChanges</string>
<reference key="source" ref="128809524"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="128809524"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">contentArray: index.indexChanges</string>
<string key="NSBinding">contentArray</string>
<string key="NSKeyPath">index.indexChanges</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">281</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">contentArray: index.indexChanges</string>
<reference key="source" ref="667905213"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="667905213"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">contentArray: index.indexChanges</string>
<string key="NSBinding">contentArray</string>
<string key="NSKeyPath">index.indexChanges</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">282</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
<string key="label">value: index.amend</string>
<reference key="source" ref="18874447"/>
<reference key="destination" ref="1001"/>
<object class="NSNibBindingConnector" key="connector">
<reference key="NSSource" ref="18874447"/>
<reference key="NSDestination" ref="1001"/>
<string key="NSLabel">value: index.amend</string>
<string key="NSBinding">value</string>
<string key="NSKeyPath">index.amend</string>
<int key="NSNibBindingConnectorVersion">2</int>
</object>
</object>
<int key="connectionID">283</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@@ -1616,7 +1599,7 @@
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilderKit</string>
<string>com.apple.InterfaceBuilderKit</string>
<string>{{59, 63}, {852, 432}}</string>
<string>{{428, 510}, {852, 432}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<integer value="0" id="8"/>
<reference ref="8"/>
@@ -1680,7 +1663,7 @@
</object>
</object>
<nil key="sourceID"/>
<int key="maxID">280</int>
<int key="maxID">283</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
@@ -1764,20 +1747,16 @@
<object class="NSMutableArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>commitController</string>
<string>stagedButtonCell</string>
<string>stagedFilesController</string>
<string>stagedTable</string>
<string>unstagedButtonCell</string>
<string>unstagedFilesController</string>
<string>unstagedTable</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>PBGitCommitController</string>
<string>PBIconAndTextCell</string>
<string>NSArrayController</string>
<string>NSTableView</string>
<string>PBIconAndTextCell</string>
<string>NSArrayController</string>
<string>NSTableView</string>
</object>
+80
View File
@@ -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 <Cocoa/Cocoa.h>
@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
+630
View File
@@ -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 -- <files>
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
-18
View File
@@ -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
+32 -152
View File
@@ -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
-1
View File
@@ -27,5 +27,4 @@
- (void) setStateMessage:(NSString *)state;
- (void) showMultiple:(NSArray *)files;
- (void) setContextSize:(int)size;
@end
+7 -17
View File
@@ -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
+8 -13
View File
@@ -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);