| /** | |
| * @license | |
| * Copyright (C) 2016 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the 'License'); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an 'AS IS' BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| (function(window, GrDiffGroup, GrDiffLine) { | |
| 'use strict'; | |
| // Prevent redefinition. | |
| if (window.GrDiffBuilder) { return; } | |
| /** | |
| * In JS, unicode code points above 0xFFFF occupy two elements of a string. | |
| * For example '𐀏'.length is 2. An occurence of such a code point is called a | |
| * surrogate pair. | |
| * | |
| * This regex segments a string along tabs ('\t') and surrogate pairs, since | |
| * these are two cases where '1 char' does not automatically imply '1 column'. | |
| * | |
| * TODO: For human languages whose orthographies use combining marks, this | |
| * approach won't correctly identify the grapheme boundaries. In those cases, | |
| * a grapheme consists of multiple code points that should count as only one | |
| * character against the column limit. Getting that correct (if it's desired) | |
| * is probably beyond the limits of a regex, but there are nonstandard APIs to | |
| * do this, and proposed (but, as of Nov 2017, unimplemented) standard APIs. | |
| * | |
| * Further reading: | |
| * On Unicode in JS: https://mathiasbynens.be/notes/javascript-unicode | |
| * Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries | |
| * A proposed JS API: https://github.com/tc39/proposal-intl-segmenter | |
| */ | |
| const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/; | |
| function GrDiffBuilder(diff, comments, prefs, projectName, outputEl, layers) { | |
| this._diff = diff; | |
| this._comments = comments; | |
| this._prefs = prefs; | |
| this._projectName = projectName; | |
| this._outputEl = outputEl; | |
| this.groups = []; | |
| this._blameInfo = null; | |
| this._parentIndex = undefined; | |
| this.layers = layers || []; | |
| if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) { | |
| throw Error('Invalid tab size from preferences.'); | |
| } | |
| if (isNaN(prefs.line_length) || prefs.line_length <= 0) { | |
| throw Error('Invalid line length from preferences.'); | |
| } | |
| for (const layer of this.layers) { | |
| if (layer.addListener) { | |
| layer.addListener(this._handleLayerUpdate.bind(this)); | |
| } | |
| } | |
| } | |
| GrDiffBuilder.GroupType = { | |
| ADDED: 'b', | |
| BOTH: 'ab', | |
| REMOVED: 'a', | |
| }; | |
| GrDiffBuilder.Highlights = { | |
| ADDED: 'edit_b', | |
| REMOVED: 'edit_a', | |
| }; | |
| GrDiffBuilder.Side = { | |
| LEFT: 'left', | |
| RIGHT: 'right', | |
| }; | |
| GrDiffBuilder.ContextButtonType = { | |
| ABOVE: 'above', | |
| BELOW: 'below', | |
| ALL: 'all', | |
| }; | |
| const PARTIAL_CONTEXT_AMOUNT = 10; | |
| /** | |
| * Abstract method | |
| * @param {string} outputEl | |
| * @param {number} fontSize | |
| */ | |
| GrDiffBuilder.prototype.addColumns = function() { | |
| throw Error('Subclasses must implement addColumns'); | |
| }; | |
| /** | |
| * Abstract method | |
| * @param {Object} group | |
| */ | |
| GrDiffBuilder.prototype.buildSectionElement = function() { | |
| throw Error('Subclasses must implement buildSectionElement'); | |
| }; | |
| GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) { | |
| const element = this.buildSectionElement(group); | |
| this._outputEl.insertBefore(element, opt_beforeSection); | |
| group.element = element; | |
| }; | |
| GrDiffBuilder.prototype.renderSection = function(element) { | |
| for (let i = 0; i < this.groups.length; i++) { | |
| const group = this.groups[i]; | |
| if (group.element === element) { | |
| const newElement = this.buildSectionElement(group); | |
| group.element.parentElement.replaceChild(newElement, group.element); | |
| group.element = newElement; | |
| break; | |
| } | |
| } | |
| }; | |
| GrDiffBuilder.prototype.getGroupsByLineRange = function( | |
| startLine, endLine, opt_side) { | |
| const groups = []; | |
| for (let i = 0; i < this.groups.length; i++) { | |
| const group = this.groups[i]; | |
| if (group.lines.length === 0) { | |
| continue; | |
| } | |
| let groupStartLine = 0; | |
| let groupEndLine = 0; | |
| if (opt_side) { | |
| groupStartLine = group.lineRange[opt_side].start; | |
| groupEndLine = group.lineRange[opt_side].end; | |
| } | |
| if (groupStartLine === 0) { // Line was removed or added. | |
| groupStartLine = groupEndLine; | |
| } | |
| if (groupEndLine === 0) { // Line was removed or added. | |
| groupEndLine = groupStartLine; | |
| } | |
| if (startLine <= groupEndLine && endLine >= groupStartLine) { | |
| groups.push(group); | |
| } | |
| } | |
| return groups; | |
| }; | |
| GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side, | |
| opt_root) { | |
| const root = Polymer.dom(opt_root || this._outputEl); | |
| const sideSelector = opt_side ? ('.' + opt_side) : ''; | |
| return root.querySelector('td.lineNum[data-value="' + lineNumber + | |
| '"]' + sideSelector + ' ~ td.content .contentText'); | |
| }; | |
| /** | |
| * Find line elements or line objects by a range of line numbers and a side. | |
| * | |
| * @param {number} start The first line number | |
| * @param {number} end The last line number | |
| * @param {string} opt_side The side of the range. Either 'left' or 'right'. | |
| * @param {!Array<GrDiffLine>} out_lines The output list of line objects. Use | |
| * null if not desired. | |
| * @param {!Array<HTMLElement>} out_elements The output list of line elements. | |
| * Use null if not desired. | |
| */ | |
| GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side, | |
| out_lines, out_elements) { | |
| const groups = this.getGroupsByLineRange(start, end, opt_side); | |
| for (const group of groups) { | |
| let content = null; | |
| for (const line of group.lines) { | |
| if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) || | |
| (opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) { | |
| continue; | |
| } | |
| const lineNumber = opt_side === 'left' ? | |
| line.beforeNumber : line.afterNumber; | |
| if (lineNumber < start || lineNumber > end) { continue; } | |
| if (out_lines) { out_lines.push(line); } | |
| if (out_elements) { | |
| if (content) { | |
| content = this._getNextContentOnSide(content, opt_side); | |
| } else { | |
| content = this.getContentByLine(lineNumber, opt_side, | |
| group.element); | |
| } | |
| if (content) { out_elements.push(content); } | |
| } | |
| } | |
| } | |
| }; | |
| /** | |
| * Re-renders the DIV.contentText elements for the given side and range of | |
| * diff content. | |
| */ | |
| GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) { | |
| const lines = []; | |
| const elements = []; | |
| let line; | |
| let el; | |
| this.findLinesByRange(start, end, side, lines, elements); | |
| for (let i = 0; i < lines.length; i++) { | |
| line = lines[i]; | |
| el = elements[i]; | |
| if (!el) { | |
| // Cannot re-render an element if it does not exist. This can happen | |
| // if lines are collapsed and not visible on the page yet. | |
| continue; | |
| } | |
| el.parentElement.replaceChild(this._createTextEl(line, side).firstChild, | |
| el); | |
| } | |
| }; | |
| GrDiffBuilder.prototype.getSectionsByLineRange = function( | |
| startLine, endLine, opt_side) { | |
| return this.getGroupsByLineRange(startLine, endLine, opt_side).map( | |
| group => { return group.element; }); | |
| }; | |
| // TODO(wyatta): Move this completely into the processor. | |
| GrDiffBuilder.prototype._insertContextGroups = function(groups, lines, | |
| hiddenRange) { | |
| const linesBeforeCtx = lines.slice(0, hiddenRange[0]); | |
| const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); | |
| const linesAfterCtx = lines.slice(hiddenRange[1]); | |
| if (linesBeforeCtx.length > 0) { | |
| groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx)); | |
| } | |
| const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); | |
| ctxLine.contextGroup = | |
| new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines); | |
| groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, | |
| [ctxLine])); | |
| if (linesAfterCtx.length > 0) { | |
| groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx)); | |
| } | |
| }; | |
| GrDiffBuilder.prototype._createContextControl = function(section, line) { | |
| if (!line.contextGroup || !line.contextGroup.lines.length) { | |
| return null; | |
| } | |
| const td = this._createElement('td'); | |
| const showPartialLinks = | |
| line.contextGroup.lines.length > PARTIAL_CONTEXT_AMOUNT; | |
| if (showPartialLinks) { | |
| td.appendChild(this._createContextButton( | |
| GrDiffBuilder.ContextButtonType.ABOVE, section, line)); | |
| td.appendChild(document.createTextNode(' - ')); | |
| } | |
| td.appendChild(this._createContextButton( | |
| GrDiffBuilder.ContextButtonType.ALL, section, line)); | |
| if (showPartialLinks) { | |
| td.appendChild(document.createTextNode(' - ')); | |
| td.appendChild(this._createContextButton( | |
| GrDiffBuilder.ContextButtonType.BELOW, section, line)); | |
| } | |
| return td; | |
| }; | |
| GrDiffBuilder.prototype._createContextButton = function(type, section, line) { | |
| const contextLines = line.contextGroup.lines; | |
| const context = PARTIAL_CONTEXT_AMOUNT; | |
| const button = this._createElement('gr-button', 'showContext'); | |
| button.setAttribute('link', true); | |
| button.setAttribute('no-uppercase', true); | |
| let text; | |
| const groups = []; // The groups that replace this one if tapped. | |
| if (type === GrDiffBuilder.ContextButtonType.ALL) { | |
| text = 'Show ' + contextLines.length + ' common line'; | |
| if (contextLines.length > 1) { text += 's'; } | |
| groups.push(line.contextGroup); | |
| } else if (type === GrDiffBuilder.ContextButtonType.ABOVE) { | |
| text = '+' + context + '↑'; | |
| this._insertContextGroups(groups, contextLines, | |
| [context, contextLines.length]); | |
| } else if (type === GrDiffBuilder.ContextButtonType.BELOW) { | |
| text = '+' + context + '↓'; | |
| this._insertContextGroups(groups, contextLines, | |
| [0, contextLines.length - context]); | |
| } | |
| Polymer.dom(button).textContent = text; | |
| button.addEventListener('tap', e => { | |
| e.detail = { | |
| groups, | |
| section, | |
| }; | |
| // Let it bubble up the DOM tree. | |
| }); | |
| return button; | |
| }; | |
| GrDiffBuilder.prototype._getCommentsForLine = function(comments, line, | |
| opt_side) { | |
| function byLineNum(lineNum) { | |
| return function(c) { | |
| return (c.line === lineNum) || | |
| (c.line === undefined && lineNum === GrDiffLine.FILE); | |
| }; | |
| } | |
| const leftComments = | |
| comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber)); | |
| const rightComments = | |
| comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber)); | |
| leftComments.forEach(c => { c.__commentSide = 'left'; }); | |
| rightComments.forEach(c => { c.__commentSide = 'right'; }); | |
| let result; | |
| switch (opt_side) { | |
| case GrDiffBuilder.Side.LEFT: | |
| result = leftComments; | |
| break; | |
| case GrDiffBuilder.Side.RIGHT: | |
| result = rightComments; | |
| break; | |
| default: | |
| result = leftComments.concat(rightComments); | |
| break; | |
| } | |
| return result; | |
| }; | |
| /** | |
| * @param {number} changeNum | |
| * @param {number|string} patchNum | |
| * @param {string} path | |
| * @param {boolean} isOnParent | |
| * @param {string} commentSide | |
| * @return {!Object} | |
| */ | |
| GrDiffBuilder.prototype.createCommentThreadGroup = function(changeNum, | |
| patchNum, path, isOnParent, commentSide) { | |
| const threadGroupEl = | |
| document.createElement('gr-diff-comment-thread-group'); | |
| threadGroupEl.changeNum = changeNum; | |
| threadGroupEl.commentSide = commentSide; | |
| threadGroupEl.patchForNewThreads = patchNum; | |
| threadGroupEl.path = path; | |
| threadGroupEl.isOnParent = isOnParent; | |
| threadGroupEl.projectName = this._projectName; | |
| threadGroupEl.parentIndex = this._parentIndex; | |
| return threadGroupEl; | |
| }; | |
| /** | |
| * @param {number} line | |
| * @param {string=} opt_side | |
| * @return {!Object} | |
| */ | |
| GrDiffBuilder.prototype._commentThreadGroupForLine = function( | |
| line, opt_side) { | |
| const comments = | |
| this._getCommentsForLine(this._comments, line, opt_side); | |
| if (!comments || comments.length === 0) { | |
| return null; | |
| } | |
| let patchNum = this._comments.meta.patchRange.patchNum; | |
| let isOnParent = comments[0].side === 'PARENT' || false; | |
| if (line.type === GrDiffLine.Type.REMOVE || | |
| opt_side === GrDiffBuilder.Side.LEFT) { | |
| if (this._comments.meta.patchRange.basePatchNum === 'PARENT' || | |
| Gerrit.PatchSetBehavior.isMergeParent( | |
| this._comments.meta.patchRange.basePatchNum)) { | |
| isOnParent = true; | |
| } else { | |
| patchNum = this._comments.meta.patchRange.basePatchNum; | |
| } | |
| } | |
| const threadGroupEl = this.createCommentThreadGroup( | |
| this._comments.meta.changeNum, patchNum, this._comments.meta.path, | |
| isOnParent, opt_side); | |
| threadGroupEl.comments = comments; | |
| if (opt_side) { | |
| threadGroupEl.setAttribute('data-side', opt_side); | |
| } | |
| return threadGroupEl; | |
| }; | |
| GrDiffBuilder.prototype._createLineEl = function( | |
| line, number, type, opt_class) { | |
| const td = this._createElement('td'); | |
| if (opt_class) { | |
| td.classList.add(opt_class); | |
| } | |
| if (line.type === GrDiffLine.Type.REMOVE) { | |
| td.setAttribute('aria-label', `${number} removed`); | |
| } else if (line.type === GrDiffLine.Type.ADD) { | |
| td.setAttribute('aria-label', `${number} added`); | |
| } | |
| if (line.type === GrDiffLine.Type.BLANK) { | |
| return td; | |
| } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) { | |
| td.classList.add('contextLineNum'); | |
| td.setAttribute('data-value', '@@'); | |
| td.textContent = '@@'; | |
| } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) { | |
| td.classList.add('lineNum'); | |
| td.setAttribute('data-value', number); | |
| td.textContent = number === 'FILE' ? 'File' : number; | |
| } | |
| return td; | |
| }; | |
| GrDiffBuilder.prototype._createTextEl = function(line, opt_side) { | |
| const td = this._createElement('td'); | |
| if (line.type !== GrDiffLine.Type.BLANK) { | |
| td.classList.add('content'); | |
| } | |
| td.classList.add(line.type); | |
| const lineLimit = | |
| !this._prefs.line_wrapping ? this._prefs.line_length : Infinity; | |
| const contentText = | |
| this._formatText(line.text, this._prefs.tab_size, lineLimit); | |
| if (opt_side) { | |
| contentText.setAttribute('data-side', opt_side); | |
| } | |
| for (const layer of this.layers) { | |
| layer.annotate(contentText, line); | |
| } | |
| td.appendChild(contentText); | |
| return td; | |
| }; | |
| /** | |
| * Returns a 'div' element containing the supplied |text| as its innerText, | |
| * with '\t' characters expanded to a width determined by |tabSize|, and the | |
| * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is | |
| * desired. | |
| * | |
| * @param {string} text The text to be formatted. | |
| * @param {number} tabSize The width of each tab stop. | |
| * @param {number} lineLimit The column after which to wrap lines. | |
| * @return {HTMLElement} | |
| */ | |
| GrDiffBuilder.prototype._formatText = function(text, tabSize, lineLimit) { | |
| const contentText = this._createElement('div', 'contentText'); | |
| let columnPos = 0; | |
| let textOffset = 0; | |
| for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) { | |
| if (segment) { | |
| // |segment| contains only normal characters. If |segment| doesn't fit | |
| // entirely on the current line, append chunks of |segment| followed by | |
| // line breaks. | |
| let rowStart = 0; | |
| let rowEnd = lineLimit - columnPos; | |
| while (rowEnd < segment.length) { | |
| contentText.appendChild( | |
| document.createTextNode(segment.substring(rowStart, rowEnd))); | |
| contentText.appendChild(this._createElement('span', 'br')); | |
| columnPos = 0; | |
| rowStart = rowEnd; | |
| rowEnd += lineLimit; | |
| } | |
| // Append the last part of |segment|, which fits on the current line. | |
| contentText.appendChild( | |
| document.createTextNode(segment.substring(rowStart))); | |
| columnPos += (segment.length - rowStart); | |
| textOffset += segment.length; | |
| } | |
| if (textOffset < text.length) { | |
| // Handle the special character at |textOffset|. | |
| if (text.startsWith('\t', textOffset)) { | |
| // Append a single '\t' character. | |
| let effectiveTabSize = tabSize - (columnPos % tabSize); | |
| if (columnPos + effectiveTabSize > lineLimit) { | |
| contentText.appendChild(this._createElement('span', 'br')); | |
| columnPos = 0; | |
| effectiveTabSize = tabSize; | |
| } | |
| contentText.appendChild(this._getTabWrapper(effectiveTabSize)); | |
| columnPos += effectiveTabSize; | |
| textOffset++; | |
| } else { | |
| // Append a single surrogate pair. | |
| if (columnPos >= lineLimit) { | |
| contentText.appendChild(this._createElement('span', 'br')); | |
| columnPos = 0; | |
| } | |
| contentText.appendChild(document.createTextNode( | |
| text.substring(textOffset, textOffset + 2))); | |
| textOffset += 2; | |
| columnPos += 1; | |
| } | |
| } | |
| } | |
| return contentText; | |
| }; | |
| /** | |
| * Returns a <span> element holding a '\t' character, that will visually | |
| * occupy |tabSize| many columns. | |
| * | |
| * @param {number} tabSize The effective size of this tab stop. | |
| * @return {HTMLElement} | |
| */ | |
| GrDiffBuilder.prototype._getTabWrapper = function(tabSize) { | |
| // Force this to be a number to prevent arbitrary injection. | |
| const result = this._createElement('span', 'tab'); | |
| result.style['tab-size'] = tabSize; | |
| result.style['-moz-tab-size'] = tabSize; | |
| result.innerText = '\t'; | |
| return result; | |
| }; | |
| GrDiffBuilder.prototype._createElement = function(tagName, className) { | |
| const el = document.createElement(tagName); | |
| // When Shady DOM is being used, these classes are added to account for | |
| // Polymer's polyfill behavior. In order to guarantee sufficient | |
| // specificity within the CSS rules, these are added to every element. | |
| // Since the Polymer DOM utility functions (which would do this | |
| // automatically) are not being used for performance reasons, this is | |
| // done manually. | |
| el.classList.add('style-scope', 'gr-diff'); | |
| if (className) { | |
| el.classList.add(className); | |
| } | |
| return el; | |
| }; | |
| GrDiffBuilder.prototype._handleLayerUpdate = function(start, end, side) { | |
| this._renderContentByRange(start, end, side); | |
| }; | |
| /** | |
| * Finds the next DIV.contentText element following the given element, and on | |
| * the same side. Will only search within a group. | |
| * @param {HTMLElement} content | |
| * @param {string} side Either 'left' or 'right' | |
| * @return {HTMLElement} | |
| */ | |
| GrDiffBuilder.prototype._getNextContentOnSide = function(content, side) { | |
| throw Error('Subclasses must implement _getNextContentOnSide'); | |
| }; | |
| /** | |
| * Determines whether the given group is either totally an addition or totally | |
| * a removal. | |
| * @param {!Object} group (GrDiffGroup) | |
| * @return {boolean} | |
| */ | |
| GrDiffBuilder.prototype._isTotal = function(group) { | |
| return group.type === GrDiffGroup.Type.DELTA && | |
| (!group.adds.length || !group.removes.length) && | |
| !(!group.adds.length && !group.removes.length); | |
| }; | |
| /** | |
| * Set the blame information for the diff. For any already-rendered line, | |
| * re-render its blame cell content. | |
| * @param {Object} blame | |
| */ | |
| GrDiffBuilder.prototype.setBlame = function(blame) { | |
| this._blameInfo = blame; | |
| // TODO(wyatta): make this loop asynchronous. | |
| for (const commit of blame) { | |
| for (const range of commit.ranges) { | |
| for (let i = range.start; i <= range.end; i++) { | |
| // TODO(wyatta): this query is expensive, but, when traversing a | |
| // range, the lines are consecutive, and given the previous blame | |
| // cell, the next one can be reached cheaply. | |
| const el = this._getBlameByLineNum(i); | |
| if (!el) { continue; } | |
| // Remove the element's children (if any). | |
| while (el.hasChildNodes()) { | |
| el.removeChild(el.lastChild); | |
| } | |
| const blame = this._getBlameForBaseLine(i, commit); | |
| el.appendChild(blame); | |
| } | |
| } | |
| } | |
| }; | |
| GrDiffBuilder.prototype.setParentIndex = function(index) { | |
| this._parentIndex = index; | |
| }; | |
| /** | |
| * Find the blame cell for a given line number. | |
| * @param {number} lineNum | |
| * @return {HTMLTableDataCellElement} | |
| */ | |
| GrDiffBuilder.prototype._getBlameByLineNum = function(lineNum) { | |
| const root = Polymer.dom(this._outputEl); | |
| return root.querySelector(`td.blame[data-line-number="${lineNum}"]`); | |
| }; | |
| /** | |
| * Given a base line number, return the commit containing that line in the | |
| * current set of blame information. If no blame information has been | |
| * provided, null is returned. | |
| * @param {number} lineNum | |
| * @return {Object} The commit information. | |
| */ | |
| GrDiffBuilder.prototype._getBlameCommitForBaseLine = function(lineNum) { | |
| if (!this._blameInfo) { return null; } | |
| for (const blameCommit of this._blameInfo) { | |
| for (const range of blameCommit.ranges) { | |
| if (range.start <= lineNum && range.end >= lineNum) { | |
| return blameCommit; | |
| } | |
| } | |
| } | |
| return null; | |
| }; | |
| /** | |
| * Given the number of a base line, get the content for the blame cell of that | |
| * line. If there is no blame information for that line, returns null. | |
| * @param {number} lineNum | |
| * @param {Object=} opt_commit Optionally provide the commit object, so that | |
| * it does not need to be searched. | |
| * @return {HTMLSpanElement} | |
| */ | |
| GrDiffBuilder.prototype._getBlameForBaseLine = function(lineNum, opt_commit) { | |
| const commit = opt_commit || this._getBlameCommitForBaseLine(lineNum); | |
| if (!commit) { return null; } | |
| const isStartOfRange = commit.ranges.some(r => r.start === lineNum); | |
| const date = (new Date(commit.time * 1000)).toLocaleDateString(); | |
| const blameNode = this._createElement('span', | |
| isStartOfRange ? 'startOfRange' : ''); | |
| const shaNode = this._createElement('span', 'sha'); | |
| shaNode.innerText = commit.id.substr(0, 7); | |
| blameNode.appendChild(shaNode); | |
| blameNode.append(` on ${date} by ${commit.author}`); | |
| return blameNode; | |
| }; | |
| /** | |
| * Create a blame cell for the given base line. Blame information will be | |
| * included in the cell if available. | |
| * @param {GrDiffLine} line | |
| * @return {HTMLTableDataCellElement} | |
| */ | |
| GrDiffBuilder.prototype._createBlameCell = function(line) { | |
| const blameTd = this._createElement('td', 'blame'); | |
| blameTd.setAttribute('data-line-number', line.beforeNumber); | |
| if (line.beforeNumber) { | |
| const content = this._getBlameForBaseLine(line.beforeNumber); | |
| if (content) { | |
| blameTd.appendChild(content); | |
| } | |
| } | |
| return blameTd; | |
| }; | |
| window.GrDiffBuilder = GrDiffBuilder; | |
| })(window, GrDiffGroup, GrDiffLine); |