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/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');