Show blame in diff
With this change a blame column is added to the left side of diff
tables. The column is empty and hidden until blame is loaded. A button
is added to the change view to trigger a load of the blame for that
diff, as well as a unload it if already loaded. In this stage, the blame
information is non-interactive and only displays the SHA, date and
commit author.
Feature: Issue 6075
Change-Id: Ifcb951265d0e6339094e6b7c9574ec9c69e60b51
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 4cf1179..06989d2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -46,8 +46,12 @@
const width = fontSize * 4;
const colgroup = document.createElement('colgroup');
+ // Add the blame column.
+ let col = this._createElement('col', 'blame');
+ colgroup.appendChild(col);
+
// Add left-side line number.
- let col = document.createElement('col');
+ col = document.createElement('col');
col.setAttribute('width', width);
colgroup.appendChild(col);
@@ -73,6 +77,8 @@
row.setAttribute('right-type', rightLine.type);
row.tabIndex = -1;
+ row.appendChild(this._createBlameCell(leftLine));
+
this._appendPair(section, row, leftLine, leftLine.beforeNumber,
GrDiffBuilder.Side.LEFT);
this._appendPair(section, row, rightLine, rightLine.afterNumber,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index 2ddb987..a033c7b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -45,8 +45,12 @@
const width = fontSize * 4;
const colgroup = document.createElement('colgroup');
+ // Add the blame column.
+ let col = this._createElement('col', 'blame');
+ colgroup.appendChild(col);
+
// Add left-side line number.
- let col = document.createElement('col');
+ col = document.createElement('col');
col.setAttribute('width', width);
colgroup.appendChild(col);
@@ -63,6 +67,8 @@
GrDiffBuilderUnified.prototype._createRow = function(section, line) {
const row = this._createElement('tr', line.type);
+ row.appendChild(this._createBlameCell(line));
+
let lineEl = this._createLineEl(line, line.beforeNumber,
GrDiffLine.Type.REMOVE);
lineEl.classList.add('left');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index f11b573..dd18b65 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -403,6 +403,11 @@
}, false);
}, false);
},
+
+ setBlame(blame) {
+ if (!this._builder || !blame) { return; }
+ this._builder.setBlame(blame);
+ },
});
})();
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 40fcf0d..e199a75 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -37,6 +37,7 @@
this._projectName = projectName;
this._outputEl = outputEl;
this.groups = [];
+ this._blameInfo = null;
this.layers = layers || [];
@@ -625,5 +626,105 @@
});
};
+ /**
+ * Set the blame information for the diff. For any already-rednered 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);
+ }
+ }
+ }
+ };
+
+ /**
+ * 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);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index c29d494..f9e465e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -1003,5 +1003,58 @@
assert.equal(result, expected);
});
});
+
+ suite('blame', () => {
+ let mockBlame;
+
+ setup(() => {
+ mockBlame = [
+ {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
+ {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
+ ];
+ });
+
+ test('setBlame attempts to render each blamed line', () => {
+ const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
+ .returns(null);
+ builder.setBlame(mockBlame);
+ assert.equal(getBlameStub.callCount, 32);
+ });
+
+ test('_getBlameCommitForBaseLine', () => {
+ builder.setBlame(mockBlame);
+ assert.isOk(builder._getBlameCommitForBaseLine(1));
+ assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1');
+
+ assert.isOk(builder._getBlameCommitForBaseLine(11));
+ assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1');
+
+ assert.isOk(builder._getBlameCommitForBaseLine(32));
+ assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2');
+
+ assert.isNull(builder._getBlameCommitForBaseLine(33));
+ });
+
+ test('_getBlameCommitForBaseLine w/o blame returns null', () => {
+ assert.isNull(builder._getBlameCommitForBaseLine(1));
+ assert.isNull(builder._getBlameCommitForBaseLine(11));
+ assert.isNull(builder._getBlameCommitForBaseLine(31));
+ });
+
+ test('_createBlameCell', () => {
+ const mocbBlameCell = document.createElement('span');
+ const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine')
+ .returns(mocbBlameCell);
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+
+ const result = builder._createBlameCell(line);
+
+ assert.isTrue(getBlameStub.calledWithExactly(3));
+ assert.equal(result.getAttribute('data-line-number'), '3');
+ assert.equal(result.firstChild, mocbBlameCell);
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
index 395c958..6699979 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -20,7 +20,8 @@
<template>
<style include="shared-styles">
.contentWrapper ::content .content,
- .contentWrapper ::content .contextControl {
+ .contentWrapper ::content .contextControl,
+ .contentWrapper ::content .blame {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -33,7 +34,8 @@
:host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .unified .right.lineNum ~ .content .contentText,
:host-context(.selected-left.selected-comment) .contentWrapper ::content .side-by-side .left + .content .message,
:host-context(.selected-right.selected-comment) .contentWrapper ::content .side-by-side .right + .content .message :not(.collapsedContent),
- :host-context(.selected-comment) .contentWrapper ::content .unified .message :not(.collapsedContent){
+ :host-context(.selected-comment) .contentWrapper ::content .unified .message :not(.collapsedContent),
+ :host-context(.selected-blame) .contentWrapper ::content .blame {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 3280b68..f00557e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -22,6 +22,7 @@
COMMENT: 'selected-comment',
LEFT: 'selected-left',
RIGHT: 'selected-right',
+ BLAME: 'selected-blame',
};
const getNewCache = () => { return {left: null, right: null}; };
@@ -66,20 +67,36 @@
_handleDown(e) {
const lineEl = this.diffBuilder.getLineElByChild(e.target);
- if (!lineEl) {
- return;
- }
- const commentSelected =
- this._elementDescendedFromClass(e.target, 'gr-diff-comment');
- const side = this.diffBuilder.getSideByLineEl(lineEl);
- const targetClasses = [];
- targetClasses.push(side === 'left' ?
- SelectionClass.LEFT :
- SelectionClass.RIGHT);
+ const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
+ if (!lineEl && !blameSelected) { return; }
- if (commentSelected) {
- targetClasses.push(SelectionClass.COMMENT);
+ const targetClasses = [];
+
+ if (blameSelected) {
+ targetClasses.push(SelectionClass.BLAME);
+ } else {
+ const commentSelected =
+ this._elementDescendedFromClass(e.target, 'gr-diff-comment');
+ const side = this.diffBuilder.getSideByLineEl(lineEl);
+
+ targetClasses.push(side === 'left' ?
+ SelectionClass.LEFT :
+ SelectionClass.RIGHT);
+
+ if (commentSelected) {
+ targetClasses.push(SelectionClass.COMMENT);
+ }
}
+
+ this._setClasses(targetClasses);
+ },
+
+ /**
+ * Set the provided list of classes on the element, to the exclusion of all
+ * other SelectionClass values.
+ * @param {!Array<!string>} targetClasses
+ */
+ _setClasses(targetClasses) {
// Remove any selection classes that do not belong.
for (const key in SelectionClass) {
if (SelectionClass.hasOwnProperty(key)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 2a35a39..a14d155 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -30,6 +30,7 @@
<gr-diff-selection>
<table id="diffTable" class="side-by-side">
<tr class="diff-row">
+ <td class="blame" data-line-number="1"></td>
<td class="lineNum left" data-value="1">1</td>
<td class="content">
<div class="contentText" data-side="left">ba ba</div>
@@ -47,6 +48,7 @@
</td>
</tr>
<tr class="diff-row">
+ <td class="blame" data-line-number="2"></td>
<td class="lineNum left" data-value="2">2</td>
<td class="content">
<div class="contentText" data-side="left">zin</div>
@@ -64,6 +66,7 @@
</td>
</tr>
<tr class="diff-row">
+ <td class="blame" data-line-number="3"></td>
<td class="lineNum left" data-value="3">3</td>
<td class="content">
<div class="contentText" data-side="left">ga ga</div>
@@ -78,6 +81,7 @@
<td class="lineNum right" data-value="3">3</td>
</tr>
<tr class="diff-row">
+ <td class="blame" data-line-number="4"></td>
<td class="lineNum left" data-value="4">4</td>
<td class="content">
<div class="contentText" data-side="left">ga ga</div>
@@ -169,6 +173,18 @@
element.classList.contains('selected-left'), 'removes selected-left');
});
+ test('applies selected-blame on blame click', () => {
+ element.classList.add('selected-left');
+ element.diffBuilder.getLineElByChild.returns(null);
+ sandbox.stub(element, '_elementDescendedFromClass',
+ (el, className) => className === 'blame');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-blame'), 'adds selected-right');
+ assert.isFalse(
+ element.classList.contains('selected-left'), 'removes selected-left');
+ });
+
test('ignores copy for non-content Element', () => {
sandbox.stub(element, '_getSelectedText');
emulateCopyOn(element.querySelector('.not-diff-row'));
@@ -203,6 +219,32 @@
['Text', 'the text'], event.clipboardData.setData.lastCall.args);
});
+ test('_setClasses adds given SelectionClass values, removes others', () => {
+ element.classList.add('selected-right');
+ element._setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(element.classList.contains('selected-comment'));
+ assert.isTrue(element.classList.contains('selected-left'));
+ assert.isFalse(element.classList.contains('selected-right'));
+ assert.isFalse(element.classList.contains('selected-blame'));
+
+ element._setClasses(['selected-blame']);
+ assert.isFalse(element.classList.contains('selected-comment'));
+ assert.isFalse(element.classList.contains('selected-left'));
+ assert.isFalse(element.classList.contains('selected-right'));
+ assert.isTrue(element.classList.contains('selected-blame'));
+ });
+
+ test('_setClasses removes before it ads', () => {
+ element.classList.add('selected-right');
+ const addStub = sandbox.stub(element.classList, 'add');
+ const removeStub = sandbox.stub(element.classList, 'remove', () => {
+ assert.isFalse(addStub.called);
+ });
+ element._setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(addStub.called);
+ assert.isTrue(removeStub.called);
+ });
+
test('copies content correctly', () => {
// Fetch the line number.
element._cachedDiffBuilder.getLineElByChild = function(child) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index f67abcc..008852e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -159,6 +159,12 @@
.editLoaded .hideOnEdit {
display: none;
}
+ .blameLoader {
+ display: none;
+ }
+ .blameLoader.show {
+ display: inline;
+ }
@media screen and (max-width: 50em) {
header {
padding: .5em var(--default-horizontal-margin);
@@ -312,6 +318,13 @@
on-tap="_handlePrefsTap">Preferences</gr-button>
</span>
</span>
+ <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _isBlameSupported)]]">
+ <span class="separator">/</span>
+ <gr-button
+ link
+ disabled="[[_isBlameLoading]]"
+ on-tap="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
+ </span>
</div>
</div>
<div class="fileNav mobile">
@@ -340,6 +353,7 @@
project-config="[[_projectConfig]]"
project-name="[[_change.project]]"
view-mode="[[_diffMode]]"
+ is-blame-loaded="{{_isBlameLoaded}}"
on-line-selected="_onLineSelected">
</gr-diff>
<gr-diff-preferences
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 3bb373f..fbd845a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -18,6 +18,8 @@
const MERGE_LIST_PATH = '/MERGE_LIST';
const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
+ const MSG_LOADING_BLAME = 'Loading blame...';
+ const MSG_LOADED_BLAME = 'Blame loaded';
const PARENT = 'PARENT';
@@ -125,6 +127,16 @@
type: Boolean,
computed: '_computeEditLoaded(_patchRange.*)',
},
+
+ _isBlameSupported: {
+ type: Boolean,
+ value: false,
+ },
+ _isBlameLoaded: Boolean,
+ _isBlameLoading: {
+ type: Boolean,
+ value: false,
+ },
},
behaviors: [
@@ -160,6 +172,10 @@
this._loggedIn = loggedIn;
});
+ this.$.restAPI.getConfig().then(config => {
+ this._isBlameSupported = config.change.allow_blame;
+ });
+
this.$.cursor.push('diffs', this.$.diff);
},
@@ -805,5 +821,36 @@
_computeContainerClass(editLoaded) {
return editLoaded ? 'editLoaded' : '';
},
+
+ _computeBlameToggleLabel(loaded, loading) {
+ if (loaded) { return 'Hide blame'; }
+ return 'Show blame';
+ },
+
+ /**
+ * Load and display blame information if it has not already been loaded.
+ * Otherwise hide it.
+ */
+ _toggleBlame() {
+ if (this._isBlameLoaded) {
+ this.$.diff.clearBlame();
+ return;
+ }
+
+ this._isBlameLoading = true;
+ this.fire('show-alert', {message: MSG_LOADING_BLAME});
+ this.$.diff.loadBlame()
+ .then(() => {
+ this._isBlameLoading = false;
+ this.fire('show-alert', {message: MSG_LOADED_BLAME});
+ })
+ .catch(() => {
+ this._isBlameLoading = false;
+ });
+ },
+
+ _computeBlameLoaderClass(isImageDiff, supported) {
+ return !isImageDiff && supported ? 'show' : '';
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 7ef5c60..7b84ef5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -212,6 +212,32 @@
#sizeWarning.warn {
display: block;
}
+ .target-row td.blame {
+ background: #eee;
+ }
+ td.blame {
+ display: none;
+ font-family: var(--font-family);
+ font-size: var(--font-size, 12px);
+ padding: 0 .5em;
+ white-space: pre;
+ }
+ :host(.showBlame) td.blame {
+ display: table-cell;
+ }
+ td.blame > span {
+ opacity: 0.6;
+ }
+ td.blame > span.startOfRange {
+ opacity: 1;
+ }
+ td.blame .sha {
+ font-family: var(--monospace-font-family);
+ }
+ .full-width td.blame {
+ overflow: hidden;
+ width: 200px;
+ }
</style>
<style include="gr-theme-default"></style>
<div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index d20d7bf..c3add28 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -16,6 +16,7 @@
const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
const ERR_INVALID_LINE = 'Invalid line number: ';
+ const MSG_EMPTY_BLAME = 'No blame information for this diff.';
const DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
@@ -125,6 +126,17 @@
},
_showWarning: Boolean,
+
+ /** @type {?Object} */
+ _blame: {
+ type: Object,
+ value: null,
+ },
+ isBlameLoaded: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeIsBlameLoaded(_blame)',
+ },
},
behaviors: [
@@ -154,6 +166,7 @@
/** @return {!Promise} */
reload() {
this.$.diffBuilder.cancel();
+ this.clearBlame();
this._safetyBypass = null;
this._showWarning = false;
this._clearDiffContent();
@@ -191,6 +204,39 @@
this.toggleClass('no-left');
},
+ /**
+ * Load and display blame information for the base of the diff.
+ * @return {Promise} A promise that resolves when blame finishes rendering.
+ */
+ loadBlame() {
+ return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
+ this.path, true)
+ .then(blame => {
+ if (!blame.length) {
+ this.fire('show-alert', {message: MSG_EMPTY_BLAME});
+ return Promise.reject(MSG_EMPTY_BLAME);
+ }
+
+ this._blame = blame;
+
+ this.$.diffBuilder.setBlame(blame);
+ this.classList.add('showBlame');
+ });
+ },
+
+ _computeIsBlameLoaded(blame) {
+ return !!blame;
+ },
+
+ /**
+ * Unload blame information for the diff.
+ */
+ clearBlame() {
+ this._blame = null;
+ this.$.diffBuilder.setBlame(null);
+ this.classList.remove('showBlame');
+ },
+
/** @return {boolean}} */
_canRender() {
return !!this.changeNum && !!this.patchRange && !!this.path &&
@@ -500,6 +546,8 @@
_prefsChanged(prefs) {
if (!prefs) { return; }
+ this.clearBlame();
+
const stylesToUpdate = {};
if (prefs.line_wrapping) {
@@ -589,7 +637,7 @@
const isB = this._diff.meta_b &&
this._diff.meta_b.content_type.startsWith('image/');
- return this._diff.binary && (isA || isB);
+ return !!(this._diff.binary && (isA || isB));
},
/** @return {!Promise} */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index f540c34..e422354 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -959,6 +959,60 @@
});
});
});
+
+ suite('blame', () => {
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('clearBlame', () => {
+ element._blame = [];
+ const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
+ element.classList.add('showBlame');
+ element.clearBlame();
+ assert.isNull(element._blame);
+ assert.isTrue(setBlameSpy.calledWithExactly(null));
+ assert.isFalse(element.classList.contains('showBlame'));
+ });
+
+ test('loadBlame', () => {
+ const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
+ .returns(Promise.resolve(mockBlame));
+ element.changeNum = 42;
+ element.patchRange = {patchNum: 5, basePatchNum: 4};
+ element.path = 'foo/bar.baz';
+ return element.loadBlame().then(() => {
+ assert.isTrue(getBlameStub.calledWithExactly(
+ 42, 5, 'foo/bar.baz', true));
+ assert.isFalse(showAlertStub.called);
+ assert.equal(element._blame, mockBlame);
+ assert.isTrue(element.classList.contains('showBlame'));
+ });
+ });
+
+ test('loadBlame empty', () => {
+ const mockBlame = [];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ sandbox.stub(element.$.restAPI, 'getBlame')
+ .returns(Promise.resolve(mockBlame));
+ element.changeNum = 42;
+ element.patchRange = {patchNum: 5, basePatchNum: 4};
+ element.path = 'foo/bar.baz';
+ return element.loadBlame()
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(() => {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isNull(element._blame);
+ assert.isFalse(element.classList.contains('showBlame'));
+ });
+ });
+ });
});
a11ySuite('basic');
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 06a3fcf..4e0f3b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1865,5 +1865,21 @@
opt_params, opt_options);
});
},
+
+ /**
+ * Get blame information for the given diff.
+ * @param {string|number} changeNum
+ * @param {string|number} patchNum
+ * @param {string} path
+ * @param {boolean=} opt_base If true, requests blame for the base of the
+ * diff, rather than the revision.
+ * @return {!Promise<!Object>}
+ */
+ getBlame(changeNum, patchNum, path, opt_base) {
+ const encodedPath = encodeURIComponent(path);
+ return this._getChangeURLAndFetch(changeNum,
+ `/files/${encodedPath}/blame`, patchNum, undefined, undefined,
+ opt_base ? {base: 't'} : undefined);
+ },
});
})();