diff --git a/Classes/Controllers/PBDiffWindowController.h b/Classes/Controllers/PBDiffWindowController.h index b8dba7ec6..71afb0e42 100644 --- a/Classes/Controllers/PBDiffWindowController.h +++ b/Classes/Controllers/PBDiffWindowController.h @@ -9,11 +9,13 @@ #import @class PBGitCommit; +@class PBGitRepository; @interface PBDiffWindowController : NSWindowController { NSString *diff; } ++ (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(NSString *)startCommit diffCommit:(NSString *)diffCommit repository:(PBGitRepository*) repository; + (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(PBGitCommit *)startCommit diffCommit:(PBGitCommit *)diffCommit; - (id) initWithDiff:(NSString *)diff; diff --git a/Classes/Controllers/PBDiffWindowController.m b/Classes/Controllers/PBDiffWindowController.m index 8b525bd8a..fc56af730 100644 --- a/Classes/Controllers/PBDiffWindowController.m +++ b/Classes/Controllers/PBDiffWindowController.m @@ -25,16 +25,9 @@ - (id) initWithDiff:(NSString *)aDiff return self; } - -+ (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(PBGitCommit *)startCommit diffCommit:(PBGitCommit *)diffCommit ++ (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(NSString *)startCommit diffCommit:(NSString *)diffCommit repository:(PBGitRepository*) repository { - if (!startCommit) - return; - - if (!diffCommit) - diffCommit = [startCommit.repository headCommit]; - - NSString *commitSelector = [NSString stringWithFormat:@"%@..%@", [startCommit realSha], [diffCommit realSha]]; + NSString *commitSelector = [NSString stringWithFormat:@"%@..%@", startCommit, diffCommit]; NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"diff", @"--no-ext-diff", commitSelector, nil]; if (![PBGitDefaults showWhitespaceDifferences]) @@ -46,7 +39,7 @@ + (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(PBGitCommit *) } int retValue; - NSString *diff = [startCommit.repository outputInWorkdirForArguments:arguments retValue:&retValue]; + NSString *diff = [repository outputInWorkdirForArguments:arguments retValue:&retValue]; if (retValue) { NSLog(@"diff failed with retValue: %d for command: '%@' output: '%@'", retValue, [arguments componentsJoinedByString:@" "], diff); return; @@ -56,5 +49,16 @@ + (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(PBGitCommit *) [diffController showWindow:nil]; } ++ (void) showDiffWindowWithFiles:(NSArray *)filePaths fromCommit:(PBGitCommit *)startCommit diffCommit:(PBGitCommit *)diffCommit +{ + if (!startCommit) + return; + + if (!diffCommit) + diffCommit = [startCommit.repository headCommit]; + + [PBDiffWindowController showDiffWindowWithFiles:filePaths fromCommit:[startCommit realSHA] diffCommit:[diffCommit realSHA] repository:[startCommit repository]]; +} + @end diff --git a/Classes/Controllers/PBRefController.h b/Classes/Controllers/PBRefController.h index a9c11f4c2..e9169cf41 100644 --- a/Classes/Controllers/PBRefController.h +++ b/Classes/Controllers/PBRefController.h @@ -46,5 +46,7 @@ - (NSArray *) menuItemsForCommit:(PBGitCommit *)commit; - (NSArray *)menuItemsForRow:(NSInteger)rowIndex; +// invoked via Cocoa Bindings +- (void)onDoubleClick:(NSArray*)selectedObjects; @end diff --git a/Classes/Controllers/PBRefController.m b/Classes/Controllers/PBRefController.m index ec27403a4..a5b7d2e06 100644 --- a/Classes/Controllers/PBRefController.m +++ b/Classes/Controllers/PBRefController.m @@ -333,6 +333,26 @@ - (NSArray *)menuItemsForRow:(NSInteger)rowIndex # pragma mark Tableview delegate methods +- (BOOL)tryToDragPatch:(PBGitCommit*)commitref + fromPoint:(NSPoint)dragPosition + inTable:(PBCommitList *)tv + toPasteboard:(NSPasteboard*)pboard +{ + NSRect imageLocation; + + dragPosition.x -= 16; + dragPosition.y -= 16; + imageLocation.origin = dragPosition; + imageLocation.size = NSMakeSize(32,32); + + [pboard declareTypes:[NSArray arrayWithObjects:@"PBGitCommit", NSFilesPromisePboardType, nil] owner:self]; + [pboard setPropertyList:[NSArray arrayWithObject:@"patch"] forType:NSFilesPromisePboardType]; + + NSData* data=[NSKeyedArchiver archivedDataWithRootObject:[commitref realSHA]]; + [pboard setData:data forType:@"PBGitCommit"]; + return YES; +} + - (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pboard { @@ -341,7 +361,7 @@ - (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexe int column = [tv columnAtPoint:location]; int subjectColumn = [tv columnWithIdentifier:@"SubjectColumn"]; if (column != subjectColumn) - return NO; + return [self tryToDragPatch:[[commitController arrangedObjects] objectAtIndex:row] fromPoint:location inTable:(PBCommitList*)tv toPasteboard:pboard]; PBGitRevisionCell *cell = (PBGitRevisionCell *)[tv preparedCellAtColumn:column row:row]; NSRect cellFrame = [tv frameOfCellAtColumn:column row:row]; @@ -349,14 +369,14 @@ - (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexe int index = [cell indexAtX:(location.x - cellFrame.origin.x)]; if (index == -1) - return NO; + return [self tryToDragPatch:[[commitController arrangedObjects] objectAtIndex:row] fromPoint:location inTable:(PBCommitList*)tv toPasteboard:pboard]; PBGitRef *ref = [[[cell objectValue] refs] objectAtIndex:index]; if ([ref isTag] || [ref isRemoteBranch]) - return NO; + return [self tryToDragPatch:[[commitController arrangedObjects] objectAtIndex:row] fromPoint:location inTable:(PBCommitList*)tv toPasteboard:pboard]; if ([[[historyController.repository headRef] ref] isEqualToRef:ref]) - return NO; + return [self tryToDragPatch:[[commitController arrangedObjects] objectAtIndex:row] fromPoint:location inTable:(PBCommitList*)tv toPasteboard:pboard]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[NSArray arrayWithObjects:[NSNumber numberWithInt:row], [NSNumber numberWithInt:index], NULL]]; [pboard declareTypes:[NSArray arrayWithObject:@"PBGitRef"] owner:self]; @@ -380,6 +400,31 @@ - (NSDragOperation)tableView:(NSTableView*)tv return NSDragOperationNone; } +- (NSArray *) tableView:(NSTableView *)aTableView +namesOfPromisedFilesDroppedAtDestination:(NSURL *)destination + forDraggedRowsWithIndexes:(NSIndexSet *)indexSet +{ + NSMutableArray *ret=[NSMutableArray arrayWithCapacity:1]; + + NSInteger idx = [indexSet firstIndex]; + while (idx != NSNotFound) { + PBGitCommit* aCommit=[[commitController arrangedObjects] objectAtIndex:idx]; + + // name the patch file based on the commit subject line (similar to what git format-patch does) + NSString* filename=[NSString stringWithFormat:@"%@.patch", [[aCommit subject] stringByReplacingOccurrencesOfString:@" " withString:@"-"]]; + + // xxx maybe bad if this is a big patch; however, doing this inside draggedImage:endedAt:operation: did not work + NSData *data = [[aCommit fullpatch] dataUsingEncoding:NSUTF8StringEncoding]; + NSString *fullname=[NSString stringWithFormat:@"%@/%@", [destination path], filename]; + [data writeToFile:fullname atomically:YES]; + + [ret addObject:filename]; + idx = [indexSet indexGreaterThanIndex: idx]; + } + + return ret; +} + - (void) dropRef:(NSDictionary *)dropInfo { PBGitRef *ref = [dropInfo objectForKey:@"dragRef"]; @@ -468,4 +513,12 @@ - (void)dealloc { historyController = nil; } +- (void)onDoubleClick:(NSArray*)selectedObjects +{ + //NSLog(@"PBRefController %@", selectedObjects); + for (PBGitCommit* commit in selectedObjects) { + [PBDiffWindowController showDiffWindowWithFiles:nil fromCommit:[[[commit parents] firstObject] SHA] diffCommit:[commit realSha] repository:[commit repository]]; + } +} + @end diff --git a/Classes/Controllers/PBWebHistoryController.m b/Classes/Controllers/PBWebHistoryController.m index ce95b9432..cc657e175 100644 --- a/Classes/Controllers/PBWebHistoryController.m +++ b/Classes/Controllers/PBWebHistoryController.m @@ -71,7 +71,7 @@ - (void) changeContentTo: (PBGitCommit *) content // 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", @"--pretty=raw", @"-M", @"--no-color", [currentSha SHA], nil]; + NSMutableArray *taskArguments = [NSMutableArray arrayWithObjects:@"show", @"--numstat", @"-C", @"-C", @"--summary", @"--pretty=raw", [currentSha SHA], nil]; if (![PBGitDefaults showWhitespaceDifferences]) [taskArguments insertObject:@"-w" atIndex:1]; @@ -79,11 +79,11 @@ - (void) changeContentTo: (PBGitCommit *) content 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]; + [nc addObserver:self selector:@selector(commitSummaryLoaded:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; [handle readToEndOfFileInBackgroundAndNotify]; } -- (void)commitDetailsLoaded:(NSNotification *)notification +- (void)commitSummaryLoaded:(NSNotification *)notification { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:nil]; @@ -91,14 +91,44 @@ - (void)commitDetailsLoaded:(NSNotification *)notification if (!data) return; - NSString *details = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - if (!details) - details = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding]; + NSString *summary = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (!summary) + summary = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding]; - if (!details) + if (!summary) return; - [[view windowScriptObject] callWebScriptMethod:@"loadCommitDetails" withArguments:[NSArray arrayWithObject:details]]; + [[view windowScriptObject] callWebScriptMethod:@"loadCommitSummary" withArguments:[NSArray arrayWithObject:summary]]; + + // Now load the full diff + NSMutableArray *taskArguments = [NSMutableArray arrayWithObjects:@"show", @"--pretty=raw", @"-C", @"-C", @"--no-color", [currentSha SHA], 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(commitFullDiffLoaded:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle]; + [handle readToEndOfFileInBackgroundAndNotify]; +} + +- (void)commitFullDiffLoaded:(NSNotification *)notification +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:nil]; + + NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem]; + if (!data) + return; + + NSString *fullDiff = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (!fullDiff) + fullDiff = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding]; + + if (!fullDiff) + return; + + [[view windowScriptObject] callWebScriptMethod:@"loadCommitFullDiff" withArguments:[NSArray arrayWithObject:fullDiff]]; } - (void)selectCommit:(NSString *)sha diff --git a/Classes/git/PBGitCommit.h b/Classes/git/PBGitCommit.h index 1ae528207..7a3c8ecf2 100644 --- a/Classes/git/PBGitCommit.h +++ b/Classes/git/PBGitCommit.h @@ -29,6 +29,7 @@ extern NSString * const kGitXCommitType; @property (nonatomic, strong, readonly) NSString *committer; @property (nonatomic, strong, readonly) NSString *details; @property (nonatomic, strong, readonly) NSString *patch; +@property (nonatomic, strong, readonly) NSString *fullpatch; @property (nonatomic, strong, readonly) NSString *realSHA; @property (nonatomic, strong, readonly) NSString *SVNRevision; diff --git a/Classes/git/PBGitCommit.m b/Classes/git/PBGitCommit.m index c0b59aeb5..1223d8dc3 100644 --- a/Classes/git/PBGitCommit.m +++ b/Classes/git/PBGitCommit.m @@ -21,6 +21,7 @@ @interface PBGitCommit () @property (nonatomic, strong) NSArray *parents; @property (nonatomic, strong) NSString *patch; +@property (nonatomic, strong) NSString *fullpatch; @property (nonatomic, strong) GTOID *sha; @end @@ -175,6 +176,18 @@ - (NSString *) patch return self->_patch; } +// same as above, but also uses --full-index --binary +- (NSString *) fullpatch +{ + if (self->_fullpatch != nil) + return _fullpatch; + + NSString *p = [self.repository outputForArguments:[NSArray arrayWithObjects:@"format-patch", @"-1", @"--stdout", @"--full-index", @"--binary", [self realSha], nil]]; + // Add a GitX identifier to the patch ;) + self.fullpatch = [[p substringToIndex:[p length] -1] stringByAppendingString:@"+GitX"]; + return self->_fullpatch; +} + - (PBGitTree*) tree { return [PBGitTree rootForCommit: self]; diff --git a/Resources/XIBs/PBGitHistoryView.xib b/Resources/XIBs/PBGitHistoryView.xib index bc9b9699c..ad982197b 100644 --- a/Resources/XIBs/PBGitHistoryView.xib +++ b/Resources/XIBs/PBGitHistoryView.xib @@ -1620,6 +1620,47 @@ 445 + + + doubleClickArgument: selectedObjects + + + + + + doubleClickArgument: selectedObjects + doubleClickArgument + selectedObjects + + NSSelectorName + onDoubleClick: + + 2 + + + 521 + + + + doubleClickTarget: self + + + + + + doubleClickTarget: self + doubleClickTarget + self + + NSSelectorName + onDoubleClick: + + + 2 + + + 524 + controller diff --git a/html/views/history/history.css b/html/views/history/history.css index 8679261f6..5cbc177e8 100644 --- a/html/views/history/history.css +++ b/html/views/history/history.css @@ -86,44 +86,6 @@ a.servicebutton:hover { padding: 10px; } -#files { - margin-top: 1em; - margin-left: 0.5em; -} - -#files a { - color: #666666; - text-decoration: none; -} - -#files a:hover { - color: #4444ff; - border-bottom: 1px solid #4444ff; -} - -#files img { - float: left; - margin-right: 0.5em; - margin-top: 1px; - width: 12px; - height: 12px; -} - -#files p { - margin-top: 0.25em; - margin-bottom: 0.25em; -} - -#files a .renamed .meta, -#files a .renamed .old { - color: #888888; - border-bottom: 1px solid #ffffff; -} - -#files a .renamed .meta { - font-weight: bold; -} - .clear_both { clear:both; display:block; @@ -153,7 +115,19 @@ a { a.showdiff { text-decoration: none; + text-align: center; font-size: 1.3em; + background-color: #d5d5d5; + display: block; + padding: 5px 10px; + width: 400px; + margin: 25px auto 25px auto; + -webkit-border-radius: 10px; + border: 1px solid #999; + -webkit-box-shadow: 1px 1px 2px #DDD; +} +a.showdiff:hover { + background-color: #c4daff; } .refs { @@ -180,6 +154,52 @@ a.showdiff { .refs.currentBranch { background-color: #fca64f; } +#message_files { + margin: 0; + padding: 0; +} +#message { + padding: 10px; +} +#files { + margin: 10px; + padding: 10px 0; + background-color: #f9f9f9; + -webkit-border-radius: 10px; + border: 1px solid #CCC; + -webkit-box-shadow: 1px 1px 2px #DDD; +} +#files ul { + margin: 0; + padding: 0; +} +#files li { + list-style-type: none; + padding: 4px 10px; + color: #000; + height: 18px; +} +#files li.odd { + background-color: #f9f9f9; +} +#files li.even { + background-color: #f0f0f0; +} +#files li:hover { + background-color: #d4e4ff; + cursor: pointer; +} +#files img.changetype-icon { + float: left; + margin: 3px 7px; +} +#files li .filename { + position: relative; + top: 2px; + font-size: 12px; + text-shadow: white 0 0 4px; + z-index: 100; +} .top-link { text-align: right; @@ -188,6 +208,71 @@ a.showdiff { font-size: 90%; } +#files .diffstat-info { + padding: 0; + position: absolute; + right: 30px; + display: inline-block; +} +#files .changes-bar { + height: 6px; + display: inline-block; + right: 46px; + position: absolute; + -webkit-border-top-left-radius: 5px; + -webkit-border-bottom-left-radius: 5px; + -webkit-box-shadow: 0 1px 1px #fff; +} +#files .changes-bar.added { + background-color: #1a843d; + border: 1px solid #164b27; +} +#files .changes-bar.removed { + top: 10px; + background-color: #ce2e2b; + border: 1px solid #8c1815; +} + +#files .diffstat-numbers { + background-color: rgba(0,0,0,.75); + color: #e3e3e3; + font-size: 12px; + font-weight: bold; + text-shadow: #000 0 1px 1px; + white-space: nowrap; + padding: 1px 5px; + position: absolute; + height: 14px; + border: 1px solid black; + -webkit-box-shadow: 0 1px 1px white; +} + +#files .diffstat-numbers.summary { + -webkit-border-top-right-radius: 12px; + -webkit-border-bottom-right-radius: 12px; + right: 0px; + width: 35px; +} +#files .diffstat-numbers.details { + -webkit-border-top-left-radius: 12px; + -webkit-border-bottom-left-radius: 12px; + right: 46px; + text-align: right; +} +#files .diffstat-numbers.details .added { + color: #5db15d; +} +#files .diffstat-numbers.details .removed { + color: #f34b4e; +} + +#files .diffstat-numbers.binary { + display: inline-block; + right: 30px; + -webkit-border-radius: 12px; + background-color: #a7681f; +} + /* div.button { diff --git a/html/views/history/history.js b/html/views/history/history.js index 4fdf88495..f77211244 100644 --- a/html/views/history/history.js +++ b/html/views/history/history.js @@ -1,4 +1,5 @@ -var commit; +var commit, + fileElementPrototype; // Create a new Commit object // obj: PBGitCommit object @@ -16,39 +17,124 @@ var Commit = function(obj) { // TODO: // this.author_date instant - // This can be called later with the output of - // 'git show' to fill in missing commit details (such as a diff) - this.parseDetails = function(details) { - this.raw = details; + this.parseSummary = function(summaryString) { + this.summaryRaw = summaryString; + + // Get the header info and the full commit message + var messageStart = this.summaryRaw.indexOf("\n\n") + 2; + this.header = this.summaryRaw.substring(0, messageStart); + var afterHeader = this.summaryRaw.substring(messageStart); + var numstatStart = afterHeader.indexOf("\n\n") + 2; + if (numstatStart > 1) { + this.message = afterHeader.substring(0, numstatStart).replace(/^ /gm, "").escapeHTML();; + var afterMessage = afterHeader.substring(numstatStart); + var filechangeStart = afterMessage.indexOf("\n ") + 1; + if (filechangeStart > 1) { + this.numstatRaw = afterMessage.substring(0, filechangeStart); + this.filechangeRaw = afterMessage.substring(filechangeStart); + } + else { + this.numstatRaw = afterMessage; + this.filechangeRaw = ""; + } + } + else { + this.message = afterHeader; + this.numstatRaw = ""; + this.filechangeRaw = ""; + } + + + if (typeof this.header !== 'undefined') { + var matches = this.header.match(/\nauthor (.*) <(.*@.*|.*)> ([0-9].*)/); + if (matches !== null && matches[2] !== null) { + if (!(matches[2].match(/@[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/))) + this.author_email = matches[2]; + + if (typeof matches[3] !== 'undefined') + this.author_date = new Date(parseInt(matches[3]) * 1000); + + matches = this.header.match(/\ncommitter (.*) <(.*@.*|.*)> ([0-9].*)/); + if (typeof matches[2] !== 'undefined') + this.committer_email = matches[2]; + if (typeof matches[3] !== 'undefined') + this.committer_date = new Date(parseInt(matches[3]) * 1000); + } + } + + // Parse the --numstat part to get the list of files and lines changed. + this.filesInfo = []; + var lines = this.numstatRaw.split('\n'); + for (var lineno=0; lineno < lines.length; lineno++) { + var columns = lines[lineno].split('\t'); + if (columns.length >= 2) { + if (columns[0] == "-" && columns[1] == "-") { + this.filesInfo.push({"filename": columns[2], + "changeType": "modified", + "binary": true}); + } + else { + this.filesInfo.push({"numLinesAdded": parseInt(columns[0]), + "numLinesRemoved": parseInt(columns[1]), + "filename": columns[2], + "changeType": "modified", + "binary": false}); + } + } + } + + // Parse the filechange part (from --summary) to get info about files + // that were added/deleted/etc. + // Sample text: + // create mode 100644 GitXTextFieldCell.h + // delete mode 100644 someDir/filename with spaces.txt + // rename fielname with spaces.txt => filename_without_spaces.txt (98%) + var lines = this.filechangeRaw.split('\n'); + for (var lineno=0; lineno < lines.length; lineno++) { + var line = lines[lineno]; + var filename="", changeType=""; + if (line.indexOf("delete") == 1) { + filename = line.match(/ delete mode \d+ (.*)$/)[0]; + changeType = "removed"; + } + else if (line.indexOf("create") == 1) { + filename = line.match(/ create mode \d+ (.*)/)[0]; + changeType = "added"; + } + else if (line.indexOf("rename") == 1) { + // get the new name of the file (the part after the " => ") + filename = line.match(/ rename (.*) \(.+\)$/)[0]; + changeType = "renamed"; + } + + if (filename != "") { + // Update the appropriate filesInfo with the actual changeType. + for (var i=0; i < commit.filesInfo.length; i+=1) { + if (commit.filesInfo[i].filename == filename) { + commit.filesInfo[i].changeType = changeType; + if (changeType == "renamed") { + var names = filename.split(" => "); + commit.filesInfo[i].oldFilename = names[0]; + commit.filesInfo[i].newFilename = names[1]; + } + break; + } + } + } + } + } - var diffStart = this.raw.indexOf("\ndiff "); - var messageStart = this.raw.indexOf("\n\n") + 2; + // This can be called later with the output of + // 'git show' to get the full diff + this.parseFullDiff = function(fullDiff) { + this.fullDiffRaw = fullDiff; + var diffStart = this.fullDiffRaw.indexOf("\ndiff "); if (diffStart > 0) { - this.message = this.raw.substring(messageStart, diffStart).replace(/^ /gm, "").escapeHTML(); - this.diff = this.raw.substring(diffStart); + this.diff = this.fullDiffRaw.substring(diffStart); } else { - this.message = this.raw.substring(messageStart).replace(/^ /gm, "").escapeHTML(); this.diff = ""; } - this.header = this.raw.substring(0, messageStart); - - if (typeof this.header !== 'undefined') { - var match = this.header.match(/\nauthor (.*) <(.*@.*|.*)> ([0-9].*)/); - if (typeof match !== 'undefined' && typeof match[2] !== 'undefined') { - if (!(match[2].match(/@[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/))) - this.author_email = match[2]; - - if (typeof match[3] !== 'undefined') - this.author_date = new Date(parseInt(match[3]) * 1000); - - match = this.header.match(/\ncommitter (.*) <(.*@.*|.*)> ([0-9].*)/); - if (typeof match[2] !== 'undefined') - this.committer_email = match[2]; - if (typeof match[3] !== 'undefined') - this.committer_date = new Date(parseInt(match[3]) * 1000); - } - } } this.reloadRefs = function() { @@ -57,6 +143,13 @@ var Commit = function(obj) { }; +var extractPrototypes = function() { + // Grab an element from the DOM, save it in a global variable (with its + // id removed) so it can be copied later, and remove it from the DOM. + fileElementPrototype = $('file_prototype'); + fileElementPrototype.removeAttribute('id'); + fileElementPrototype.parentNode.removeChild(fileElementPrototype); +} var confirm_gist = function(confirmation_message) { if (!Controller.isFeatureEnabled_("confirmGist")) { @@ -161,9 +254,9 @@ var showRefs = function() { var loadCommit = function(commitObject, currentRef) { // These are only the things we can do instantly. - // Other information will be loaded later by loadCommitDetails, - // Which will be called from the controller once - // the commit details are in. + // Other information will be loaded later by loadCommitSummary + // and loadCommitFullDiff, which will be called from the + // controller once the commit details are in. if (commit && commit.notificationID) clearTimeout(commit.notificationID); @@ -174,10 +267,13 @@ var loadCommit = function(commitObject, currentRef) { $("commitID").innerHTML = commit.sha; $("authorID").innerHTML = commit.author_name; $("subjectID").innerHTML = commit.subject.escapeHTML(); - $("diff").innerHTML = "" - $("message").innerHTML = "" - $("files").innerHTML = "" - $("date").innerHTML = "" + $("diff").innerHTML = ""; + $("message").innerHTML = ""; + $("date").innerHTML = ""; + $("files").style.display = "none"; + var filelist = $("filelist"); + while (filelist.hasChildNodes()) + filelist.removeChild(filelist.lastChild); showRefs(); for (var i = 0; i < $("commit_header").rows.length; ++i) { @@ -245,67 +341,7 @@ var formatRenameDiff = function(d) { var showDiff = function() { - $("files").innerHTML = ""; - // Callback for the diff highlighter. Used to generate a filelist - var newfile = function(name1, name2, id, mode_change, old_mode, new_mode) { - var img = document.createElement("img"); - var p = document.createElement("p"); - var link = document.createElement("a"); - link.setAttribute("href", "#" + id); - p.appendChild(link); - var finalFile = ""; - var renamed = false; - if (name1 == name2) { - finalFile = name1; - img.src = "../../images/modified.svg"; - img.title = "Modified file"; - p.title = "Modified file"; - if (mode_change) - p.appendChild(document.createTextNode(" mode " + old_mode + " → " + new_mode)); - } - else if (name1 == "/dev/null") { - img.src = "../../images/added.svg"; - img.title = "Added file"; - p.title = "Added file"; - finalFile = name2; - } - else if (name2 == "/dev/null") { - img.src = "../../images/removed.svg"; - img.title = "Removed file"; - p.title = "Removed file"; - finalFile = name1; - } - else { - renamed = true; - } - if (renamed) { - img.src = "../../images/renamed.svg"; - img.title = "Renamed file"; - p.title = "Renamed file"; - finalFile = name2; - var rfd = renameDiff(name1.unEscapeHTML(), name2.unEscapeHTML()); - var html = [ - '', - rfd[0].escapeHTML(), - ' { ', - '', rfd[1].escapeHTML(), '', - ' -> ', - '', rfd[2].escapeHTML(), '', - ' } ', - rfd[3].escapeHTML(), - '' - ].join(""); - link.innerHTML = html; - } else { - link.appendChild(document.createTextNode(finalFile.unEscapeHTML())); - } - link.setAttribute("representedFile", finalFile); - - p.insertBefore(img, link); - $("files").appendChild(p); - } - var binaryDiff = function(filename) { if (filename.match(/\.(png|jpg|icns|psd)$/i)) return 'Display image'; @@ -313,7 +349,7 @@ var showDiff = function() { return "Binary file differs"; } - highlightDiff(commit.diff, $("diff"), { "newfile" : newfile, "binaryFile" : binaryDiff }); + highlightDiff(commit.diff, $("diff"), { "binaryFile" : binaryDiff }); } var showImage = function(element, filename) @@ -338,27 +374,27 @@ var enableFeatures = function() enableFeature("gravatar", $("committer_gravatar").parentNode) } -var loadCommitDetails = function(data) +var loadCommitSummary = function(data) { - commit.parseDetails(data); - + commit.parseSummary(data); + if (commit.notificationID) clearTimeout(commit.notificationID) - else - $("notification").style.display = "none"; - + else + $("notification").style.display = "none"; + var formatEmail = function(name, email) { return email ? name + " <" + email + ">" : name; } - + $("authorID").innerHTML = formatEmail(commit.author_name, commit.author_email); $("date").innerHTML = commit.author_date; setGravatar(commit.author_email, $("author_gravatar")); - + if (commit.committer_name != commit.author_name) { $("committerID").parentNode.style.display = ""; $("committerID").innerHTML = formatEmail(commit.committer_name, commit.committer_email); - + $("committerDate").parentNode.style.display = ""; $("committerDate").innerHTML = commit.committer_date; setGravatar(commit.committer_email, $("committer_gravatar")); @@ -369,10 +405,108 @@ var loadCommitDetails = function(data) $("message").innerHTML = commit.message.replace(/\b(https?:\/\/[^\s<]*)/ig, "$1").replace(/\n/g,"
"); + if (commit.filesInfo.length > 0) { + // Create the file list + for (var i=0; i < commit.filesInfo.length; i+=1) { + var fileInfo = commit.filesInfo[i]; + var fileElem = fileElementPrototype.cloneNode(true); // this is a
  • + fileElem.targetFileId = "file_index_"+i; + + var displayName, representedFile; + if (fileInfo.changeType == "renamed") { + displayName = fileInfo.filename; + representedFile = fileInfo.newFilename; + } + else { + displayName = fileInfo.filename; + representedFile = fileInfo.filename; + } + fileElem.title = fileInfo.changeType + ": " + displayName; // set tooltip + fileElem.setAttribute("representedFile", representedFile); + + if (i % 2) + fileElem.className += "even"; + else + fileElem.className += "odd"; + fileElem.onclick = function () { + // Show the full diff in case it's not already visisble. + showDiff(); + // Scroll to that file. + $(this.targetFileId).scrollIntoView(true); + } + + // Start with a modified icon, and update it later when the + // `diff --summary` info comes back. + var imgElement = fileElem.getElementsByClassName("changetype-icon")[0]; + imgElement.src = "../../images/"+fileInfo.changeType+".svg"; + + var filenameElement = fileElem.getElementsByClassName("filename")[0]; + filenameElement.innerText = displayName; + + var diffstatElem = fileElem.getElementsByClassName("diffstat-info")[0]; + var binaryElem = fileElem.getElementsByClassName("binary")[0] + if (fileInfo.binary) { + // remove the diffstat-info element + diffstatElem.parentNode.removeChild(diffstatElem); + } + else { + // remove the binary element + binaryElem.parentNode.removeChild(binaryElem); + + // Show the num of lines added/removed + var addedWidth = 2 * fileInfo.numLinesAdded; + var removedWidth = 2 * fileInfo.numLinesRemoved; + // Scale them down proportionally if they're too wide. + var maxWidth = 350; + var minWidth = 5; + if (addedWidth+removedWidth > maxWidth) { + var scaleBy = maxWidth/(addedWidth+removedWidth); + addedWidth *= scaleBy; + removedWidth *= scaleBy; + } + if (addedWidth > 0 && addedWidth < minWidth) addedWidth = minWidth; + if (removedWidth > 0 && removedWidth < minWidth) removedWidth = minWidth; + + // show lines changed info + var numLinesAdded = fileInfo.numLinesAdded; + var numLinesRemoved = fileInfo.numLinesRemoved; + var numLinesChanged = numLinesAdded + numLinesRemoved; + // summarize large numbers + if (numLinesChanged > 999) numLinesChanged = "~" + Math.round(numLinesChanged / 1000) + "k"; + // fill in numbers + var diffstatSummary = diffstatElem.getElementsByClassName("diffstat-numbers")[1]; + diffstatSummary.innerText = numLinesChanged; + var diffstatDetails = diffstatElem.getElementsByClassName("diffstat-numbers")[0]; + diffstatDetails.getElementsByClassName("added")[0].innerText = "+"+numLinesAdded; + diffstatDetails.getElementsByClassName("removed")[0].innerText = "-"+numLinesRemoved; + + // Size the bars + var addedBar = diffstatElem.getElementsByClassName("changes-bar")[0]; + if (addedWidth >= minWidth) + addedBar.style.width = addedWidth; + else + addedBar.style.visibility = "hidden"; + + var removedBar = diffstatElem.getElementsByClassName("changes-bar")[1]; + if (removedWidth >= minWidth) + removedBar.style.width = removedWidth; + else + removedBar.style.visibility = "hidden"; + } + $("filelist").appendChild(fileElem); + } + $("files").style.display = ""; + } +} + +var loadCommitFullDiff = function(data) +{ + commit.parseFullDiff(data); + if (commit.diff.length < 200000) showDiff(); else - $("diff").innerHTML = "This is a large commit. Click here or press 'v' to view."; + $("diff").innerHTML = "This is a large commit.
    Click here or press 'v' to view.
    "; hideNotification(); enableFeatures(); diff --git a/html/views/history/index.html b/html/views/history/index.html index d4fab2f9b..367e37137 100644 --- a/html/views/history/index.html +++ b/html/views/history/index.html @@ -10,9 +10,38 @@ + - +