mirror of
https://github.com/kennethreitz-archive/gitx.git
synced 2026-06-05 15:30:18 +00:00
362 lines
12 KiB
Objective-C
362 lines
12 KiB
Objective-C
//
|
|
// PBWebCommitController.m
|
|
//
|
|
// Created by David Catmull on 10-06-11.
|
|
//
|
|
|
|
#import "PBWebCommitController.h"
|
|
#import "PBGitCommit.h"
|
|
#import "PBGitDefaults.h"
|
|
#import "GLFileView.h"
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
|
|
@interface PBWebCommitController (Private)
|
|
|
|
- (NSArray *)parseHeader:(NSString *)text;
|
|
- (NSString *)htmlForHeader:(NSArray *)header withRefs:(NSString *)badges;
|
|
- (NSMutableDictionary *)parseStats:(NSString *)txt;
|
|
- (NSString *) arbitraryHashForString:(NSString*)concat;
|
|
|
|
@end
|
|
|
|
// -parseHeader: returns an array of dictionaries with these keys
|
|
const NSString *kHeaderKeyName = @"name";
|
|
const NSString *kHeaderKeyContent = @"content";
|
|
|
|
// Keys for the author/committer dictionary
|
|
const NSString *kAuthorKeyName = @"name";
|
|
const NSString *kAuthorKeyEmail = @"email";
|
|
const NSString *kAuthorKeyDate = @"date";
|
|
|
|
@implementation PBWebCommitController
|
|
|
|
@synthesize diff;
|
|
|
|
- (void) awakeFromNib
|
|
{
|
|
startFile = @"history";
|
|
[super awakeFromNib];
|
|
}
|
|
|
|
- (void)closeView
|
|
{
|
|
[[self script] setValue:nil forKey:@"commit"];
|
|
|
|
[super closeView];
|
|
}
|
|
|
|
- (void) didLoad
|
|
{
|
|
currentSha = nil;
|
|
}
|
|
|
|
- (void) changeContentTo: (PBGitCommit *) content
|
|
{
|
|
if (content == nil || !finishedLoading)
|
|
return;
|
|
|
|
currentSha = [content sha];
|
|
|
|
// Now we load the extended details. We used to do this in a separate thread,
|
|
// but this caused some funny behaviour because NSTask's and NSThread's don't really
|
|
// like each other. Instead, just do it async.
|
|
|
|
NSMutableArray *taskArguments = [NSMutableArray arrayWithObjects:@"show", @"--numstat", @"--summary", @"--pretty=raw", currentSha, nil];
|
|
if (![PBGitDefaults showWhitespaceDifferences])
|
|
[taskArguments insertObject:@"-w" atIndex:1];
|
|
|
|
NSFileHandle *handle = [repository handleForArguments:taskArguments];
|
|
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
// Remove notification, in case we have another one running
|
|
[nc removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:nil];
|
|
[nc addObserver:self selector:@selector(commitDetailsLoaded:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
|
|
[handle readToEndOfFileInBackgroundAndNotify];
|
|
}
|
|
|
|
- (void)commitDetailsLoaded:(NSNotification *)notification
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:nil];
|
|
|
|
NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
|
|
if (!data)
|
|
return;
|
|
|
|
NSString *details = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
if (!details)
|
|
details = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding];
|
|
|
|
if (!details)
|
|
return;
|
|
|
|
// Header
|
|
NSArray *headerItems = [self parseHeader:details];
|
|
NSString *header = [self htmlForHeader:headerItems withRefs:[self refsForCurrentCommit]];
|
|
|
|
// In case the commit is a merge, we need to explicity give diff-tree the
|
|
// list of parents, or else it will yield an empty result.
|
|
// If it's not a merge, this won't hurt.
|
|
NSMutableArray *allParents = [NSMutableArray array];
|
|
|
|
for (NSDictionary *item in headerItems)
|
|
if ([[item objectForKey:kHeaderKeyName] isEqualToString:@"parent"])
|
|
[allParents addObject:[item objectForKey:kHeaderKeyContent]];
|
|
|
|
NSArray *parents = [self chooseDiffParents:allParents];
|
|
|
|
// File Stats
|
|
NSMutableDictionary *stats = [self parseStats:details];
|
|
|
|
// File list
|
|
NSMutableArray *args = [NSMutableArray arrayWithObjects:@"diff-tree", @"--root", @"-r", @"-C90%", @"-M90%", nil];
|
|
[args addObjectsFromArray:parents];
|
|
[args addObject:currentSha];
|
|
NSString *dt = [repository outputInWorkdirForArguments:args];
|
|
NSString *fileList = [GLFileView parseDiffTree:dt withStats:stats];
|
|
|
|
// Diffs list
|
|
args = [NSMutableArray arrayWithObjects:@"diff-tree", @"--root", @"--cc", @"-C90%", @"-M90%", nil];
|
|
[args addObjectsFromArray:parents];
|
|
[args addObject:currentSha];
|
|
NSString *d = [repository outputInWorkdirForArguments:args];
|
|
NSString *diffs = [GLFileView parseDiff:d];
|
|
|
|
NSString *html = [NSString stringWithFormat:@"%@%@<div id='diffs'>%@</div>",header,fileList,diffs];
|
|
|
|
html = [html stringByReplacingOccurrencesOfString:@"{SHA_PREV}" withString:[NSString stringWithFormat:@"%@^",currentSha]];
|
|
html = [html stringByReplacingOccurrencesOfString:@"{SHA}" withString:currentSha];
|
|
|
|
[[view windowScriptObject] callWebScriptMethod:@"showCommit" withArguments:[NSArray arrayWithObject:html]];
|
|
|
|
#ifdef DEBUG_BUILD
|
|
NSString *dom = [(DOMHTMLElement*)[[[view mainFrame] DOMDocument] documentElement] outerHTML];
|
|
NSString *tmpFile = @"~/tmp/test2.html";
|
|
[dom writeToFile:[tmpFile stringByExpandingTildeInPath] atomically:true encoding:NSUTF8StringEncoding error:nil];
|
|
#endif
|
|
}
|
|
|
|
- (NSString*) refsForCurrentCommit
|
|
{
|
|
return @"";
|
|
}
|
|
|
|
- (NSArray*) chooseDiffParents:(NSArray *)parents
|
|
{
|
|
return parents;
|
|
}
|
|
|
|
- (NSMutableDictionary *)parseStats:(NSString *)txt
|
|
{
|
|
NSArray *lines = [txt componentsSeparatedByString:@"\n"];
|
|
NSMutableDictionary *stats=[NSMutableDictionary dictionary];
|
|
int black=0;
|
|
for(NSString *line in lines){
|
|
if([line length]==0){
|
|
black++;
|
|
}else if(black==2){
|
|
NSArray *file=[line componentsSeparatedByString:@"\t"];
|
|
if([file count]==3){
|
|
[stats setObject:file forKey:[file objectAtIndex:2]];
|
|
}
|
|
}
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
- (NSArray *)parseHeader:(NSString *)text
|
|
{
|
|
NSMutableArray *result = [NSMutableArray array];
|
|
NSArray *lines = [text componentsSeparatedByString:@"\n"];
|
|
BOOL parsingSubject = NO;
|
|
|
|
for (NSString *line in lines) {
|
|
if ([line length] == 0) {
|
|
if (!parsingSubject)
|
|
parsingSubject = TRUE;
|
|
else
|
|
break;
|
|
} else {
|
|
if (parsingSubject) {
|
|
NSString *trimmedLine = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
[result addObject:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
@"subject", kHeaderKeyName, trimmedLine, kHeaderKeyContent, nil]];
|
|
} else {
|
|
NSArray *comps = [line componentsSeparatedByString:@" "];
|
|
if ([comps count] == 2) {
|
|
[result addObject:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
[comps objectAtIndex:0], kHeaderKeyName,
|
|
[comps objectAtIndex:1], kHeaderKeyContent, nil]];
|
|
} else if ([comps count] > 2) {
|
|
NSRange r_email_i = [line rangeOfString:@"<"];
|
|
NSRange r_email_e = [line rangeOfString:@">"];
|
|
NSRange r_name_i = [line rangeOfString:@" "];
|
|
|
|
NSString *name = [line substringWithRange:NSMakeRange(r_name_i.location,(r_email_i.location-r_name_i.location))];
|
|
NSString *email = [line substringWithRange:NSMakeRange(r_email_i.location+1,((r_email_e.location-1)-r_email_i.location))];
|
|
|
|
NSArray *t=[[line substringFromIndex:r_email_e.location+2] componentsSeparatedByString:@" "];
|
|
NSDate *date=[NSDate dateWithTimeIntervalSince1970:[[t objectAtIndex:0] doubleValue]];
|
|
|
|
NSDictionary *content = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
name, kAuthorKeyName,
|
|
email, kAuthorKeyEmail,
|
|
date, kAuthorKeyDate,
|
|
nil];
|
|
[result addObject:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
[comps objectAtIndex:0], kHeaderKeyName,
|
|
content, kHeaderKeyContent,
|
|
nil]];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSString *)htmlForHeader:(NSArray *)header withRefs:(NSString *)badges
|
|
{
|
|
NSString *last_mail = @"";
|
|
NSMutableString *auths=[NSMutableString string];
|
|
NSMutableString *refs=[NSMutableString string];
|
|
NSMutableString *subject=[NSMutableString string];
|
|
|
|
for (NSDictionary *item in header) {
|
|
if ([[item objectForKey:kHeaderKeyName] isEqualToString:@"subject"]) {
|
|
[subject appendString:[NSString stringWithFormat:@"%@<br/>",[GLFileView escapeHTML:[item objectForKey:kHeaderKeyContent]]]];
|
|
}else{
|
|
if([[item objectForKey:kHeaderKeyContent] isKindOfClass:[NSString class]]){
|
|
[refs appendString:[NSString stringWithFormat:@"<tr><td>%@</td><td><a href='' onclick='selectCommit(this.innerHTML); return false;'>%@</a></td></tr>",[item objectForKey:kHeaderKeyName],[item objectForKey:kHeaderKeyContent]]];
|
|
}else{ // NSDictionary: author or committer
|
|
NSDictionary *content = [item objectForKey:kHeaderKeyContent];
|
|
NSString *email = [content objectForKey:kAuthorKeyEmail];
|
|
|
|
if(![email isEqualToString:last_mail]){
|
|
NSString *name = [content objectForKey:kAuthorKeyName];
|
|
NSDate *date = [content objectForKey:kAuthorKeyDate];
|
|
NSDateFormatter* theDateFormatter = [[NSDateFormatter alloc] init];
|
|
[theDateFormatter setDateStyle:NSDateFormatterMediumStyle];
|
|
[theDateFormatter setTimeStyle:NSDateFormatterMediumStyle];
|
|
NSString *dateString=[theDateFormatter stringForObjectValue:date];
|
|
|
|
[auths appendString:[NSString stringWithFormat:@"<div class='user %@ clearfix'>",[item objectForKey:kHeaderKeyName]]];
|
|
if([self isFeatureEnabled:@"gravatar"]){
|
|
NSString *hash=[self arbitraryHashForString:email];
|
|
[auths appendString:[NSString stringWithFormat:@"<img class='avatar' src='http://www.gravatar.com/avatar/%@?d=wavatar&s=30'/>",hash]];
|
|
}
|
|
[auths appendString:[NSString stringWithFormat:@"<p class='name'>%@ <span class='rol'>(%@)</span></p>",name,[item objectForKey:kHeaderKeyName]]];
|
|
[auths appendString:[NSString stringWithFormat:@"<p class='time'>%@</p></div>",dateString]];
|
|
}
|
|
last_mail=email;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [NSString stringWithFormat:@"<div id='header' class='clearfix'><table class='references'>%@</table><p class='subject'>%@</p>%@<div id='badges'>%@</div></div>",refs,subject,auths,badges];
|
|
}
|
|
|
|
- (NSString *) arbitraryHashForString:(NSString*)concat {
|
|
const char *concat_str = [concat UTF8String];
|
|
unsigned char result[CC_MD5_DIGEST_LENGTH];
|
|
CC_MD5(concat_str, strlen(concat_str), result);
|
|
|
|
NSMutableString *hash = [NSMutableString string];
|
|
|
|
int i;
|
|
for (i = 0; i < 16; i++)
|
|
[hash appendFormat:@"%02x", result[i]];
|
|
|
|
return hash;
|
|
}
|
|
|
|
- (void)selectCommit:(NSString *)sha
|
|
{
|
|
}
|
|
|
|
// TODO: this is duplicated in PBWebDiffController
|
|
- (void) openFileMerge:(NSString*)file sha:(NSString *)sha sha2:(NSString *)sha2
|
|
{
|
|
NSArray *args=[NSArray arrayWithObjects:@"difftool",@"--no-prompt",@"--tool=opendiff",sha,sha2,@"--",file,nil];
|
|
[repository handleInWorkDirForArguments:args];
|
|
}
|
|
|
|
|
|
- (void) sendKey: (NSString*) key
|
|
{
|
|
id script = [view windowScriptObject];
|
|
[script callWebScriptMethod:@"handleKeyFromCocoa" withArguments: [NSArray arrayWithObject:key]];
|
|
}
|
|
|
|
- (void) copySource
|
|
{
|
|
NSString *source = [(DOMHTMLElement *)[[[view mainFrame] DOMDocument] documentElement] outerHTML];
|
|
NSPasteboard *a =[NSPasteboard generalPasteboard];
|
|
[a declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self];
|
|
[a setString:source forType: NSStringPboardType];
|
|
}
|
|
|
|
- (PBGitRef*) refFromString:(NSString*)refString
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (NSArray*) menuItemsForPath:(NSString*)path
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (NSArray *) webView:(WebView *)sender
|
|
contextMenuItemsForElement:(NSDictionary *)element
|
|
defaultMenuItems:(NSArray *)defaultMenuItems
|
|
{
|
|
DOMNode *node = [element valueForKey:@"WebElementDOMNode"];
|
|
|
|
while (node) {
|
|
// Every ref has a class name of 'refs' and some other class. We check on that to see if we pressed on a ref.
|
|
if ([[node className] hasPrefix:@"refs "]) {
|
|
NSString *selectedRefString = [[[node childNodes] item:0] textContent];
|
|
PBGitRef *ref = [self refFromString:selectedRefString];
|
|
if (ref != nil)
|
|
return [contextMenuDelegate menuItemsForRef:ref];
|
|
DLog(@"Could not find selected ref!");
|
|
return defaultMenuItems;
|
|
}
|
|
if ([node hasAttributes] && [[node attributes] getNamedItem:@"representedFile"])
|
|
return [self menuItemsForPath:[[[node attributes] getNamedItem:@"representedFile"] value]];
|
|
else if ([[node class] isEqual:[DOMHTMLImageElement class]]) {
|
|
// Copy Image is the only menu item that makes sense here since we don't need
|
|
// to download the image or open it in a new window (besides with the
|
|
// current implementation these two entries can crash GitX anyway)
|
|
for (NSMenuItem *item in defaultMenuItems)
|
|
if ([item tag] == WebMenuItemTagCopyImageToClipboard)
|
|
return [NSArray arrayWithObject:item];
|
|
return nil;
|
|
}
|
|
|
|
node = [node parentNode];
|
|
}
|
|
|
|
return defaultMenuItems;
|
|
}
|
|
|
|
|
|
// Open external links in the default browser
|
|
- (void)webView:(WebView *)sender decidePolicyForNewWindowAction:(NSDictionary *)actionInformation
|
|
request:(NSURLRequest *)request
|
|
newFrameName:(NSString *)frameName
|
|
decisionListener:(id < WebPolicyDecisionListener >)listener
|
|
{
|
|
[[NSWorkspace sharedWorkspace] openURL:[request URL]];
|
|
}
|
|
|
|
- getConfig:(NSString *)config
|
|
{
|
|
return [repository valueForKeyPath:[@"config." stringByAppendingString:config]];
|
|
}
|
|
|
|
- (void) preferencesChanged
|
|
{
|
|
[[self script] callWebScriptMethod:@"enableFeatures" withArguments:nil];
|
|
}
|
|
|
|
@end
|