mirror of
https://github.com/kennethreitz-archive/gitx.git
synced 2026-06-05 23:40:18 +00:00
0823ff859d
This issues a simple reloadRefs when refreshing the commit-view, keeping the current branchname up-to-date here as well. Signed-off-by: Johannes Gilger <heipei@hackvalue.de>
443 lines
15 KiB
Objective-C
443 lines
15 KiB
Objective-C
//
|
|
// PBGitCommitController.m
|
|
// GitX
|
|
//
|
|
// Created by Pieter de Bie on 19-09-08.
|
|
// Copyright 2008 __MyCompanyName__. All rights reserved.
|
|
//
|
|
|
|
#import "PBGitCommitController.h"
|
|
#import "NSFileHandleExt.h"
|
|
#import "PBChangedFile.h"
|
|
#import "PBWebChangesController.h"
|
|
#import "NSString_RegEx.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;
|
|
@end
|
|
|
|
@implementation PBGitCommitController
|
|
|
|
@synthesize files, status, busy, amend;
|
|
|
|
- (void)awakeFromNib
|
|
{
|
|
self.files = [NSMutableArray array];
|
|
[super awakeFromNib];
|
|
[self refresh:self];
|
|
|
|
[commitMessageView setTypingAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Monaco" size:12.0] forKey:NSFontAttributeName]];
|
|
|
|
[unstagedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasUnstagedChanges == 1"]];
|
|
[cachedFilesController setFilterPredicate:[NSPredicate predicateWithFormat:@"hasStagedChanges == 1"]];
|
|
|
|
[unstagedFilesController setSortDescriptors:[NSArray arrayWithObjects:
|
|
[[NSSortDescriptor alloc] initWithKey:@"status" ascending:false],
|
|
[[NSSortDescriptor alloc] initWithKey:@"path" ascending:true], nil]];
|
|
[cachedFilesController setSortDescriptors:[NSArray arrayWithObject:
|
|
[[NSSortDescriptor alloc] initWithKey:@"path" ascending:true]]];
|
|
}
|
|
- (void) removeView
|
|
{
|
|
[webController closeView];
|
|
[super finalize];
|
|
}
|
|
- (NSResponder *)firstResponder;
|
|
{
|
|
return commitMessageView;
|
|
}
|
|
|
|
- (IBAction)signOff:(id)sender
|
|
{
|
|
if (![repository.config valueForKeyPath:@"user.name"] || ![repository.config valueForKeyPath:@"user.email"])
|
|
return [[repository windowController] showMessageSheet:@"User's name not set" infoText:@"Signing off a commit requires setting user.name and user.email in your git config"];
|
|
NSString *SOBline = [NSString stringWithFormat:@"Signed-off-by: %@ <%@>",
|
|
[repository.config valueForKeyPath:@"user.name"],
|
|
[repository.config valueForKeyPath:@"user.email"]];
|
|
|
|
if([commitMessageView.string rangeOfString:SOBline].location == NSNotFound) {
|
|
NSArray *selectedRanges = [commitMessageView selectedRanges];
|
|
commitMessageView.string = [NSString stringWithFormat:@"%@\n\n%@",
|
|
commitMessageView.string, SOBline];
|
|
[commitMessageView setSelectedRanges: selectedRanges];
|
|
}
|
|
}
|
|
|
|
- (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.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];
|
|
|
|
// Reload refs (in case HEAD changed)
|
|
[repository reloadRefs];
|
|
}
|
|
|
|
- (void) updateView
|
|
{
|
|
[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
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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"]]) {
|
|
[[repository windowController] showMessageSheet:@"Cannot commit merges" infoText:@"GitX cannot commit merges yet. Please commit your changes from the command line."];
|
|
return;
|
|
}
|
|
|
|
if ([[cachedFilesController arrangedObjects] count] == 0) {
|
|
[[repository windowController] showMessageSheet:@"No changes to commit" infoText:@"You must first stage some changes before committing"];
|
|
return;
|
|
}
|
|
|
|
NSString *commitMessage = [commitMessageView string];
|
|
if ([commitMessage length] < 3) {
|
|
[[repository windowController] showMessageSheet:@"Commitmessage missing" infoText:@"Please enter a commit message before committing"];
|
|
return;
|
|
}
|
|
|
|
[cachedFilesController setSelectionIndexes:[NSIndexSet indexSet]];
|
|
[unstagedFilesController setSelectionIndexes:[NSIndexSet indexSet]];
|
|
|
|
NSString *commitSubject;
|
|
NSRange newLine = [commitMessage rangeOfString:@"\n"];
|
|
if (newLine.location == NSNotFound)
|
|
commitSubject = commitMessage;
|
|
else
|
|
commitSubject = [commitMessage substringToIndex:newLine.location];
|
|
|
|
commitSubject = [@"commit: " stringByAppendingString:commitSubject];
|
|
|
|
NSString *commitMessageFile;
|
|
commitMessageFile = [repository.fileURL.path
|
|
stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
|
|
|
|
[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"];
|
|
|
|
int ret;
|
|
|
|
NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil];
|
|
NSString *parent = amend ? @"HEAD^" : @"HEAD";
|
|
if ([repository parseReference:parent]) {
|
|
[arguments addObject:@"-p"];
|
|
[arguments addObject:parent];
|
|
}
|
|
|
|
NSString *commit = [repository outputForArguments:arguments
|
|
inputString:commitMessage
|
|
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--;
|
|
[commitMessageView setString:@""];
|
|
amend = NO;
|
|
amendEnvironment = nil;
|
|
[self refresh:self];
|
|
self.amend = NO;
|
|
}
|
|
|
|
- (void) stageHunk:(NSString *)hunk reverse:(BOOL)reverse
|
|
{
|
|
[self processHunk:hunk stage:TRUE reverse:reverse];
|
|
}
|
|
|
|
- (void)discardHunk:(NSString *)hunk
|
|
{
|
|
[self processHunk:hunk stage:FALSE reverse:TRUE];
|
|
}
|
|
|
|
- (void)processHunk:(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];
|
|
|
|
// 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];
|
|
}
|
|
|
|
@end
|