Merge "Add support for images in diffs"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 3175517..da40a9d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -272,6 +272,7 @@
</div>
</section>
<gr-file-list id="fileList"
+ change="[[_change]]"
change-num="[[_changeNum]]"
patch-range="[[_patchRange]]"
comments="[[_comments]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 976cf9b..f15c654 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -170,6 +170,8 @@
</div>
</div>
<gr-diff hidden
+ project="[[change.project]]"
+ commit="[[change.current_revision]]"
change-num="[[changeNum]]"
patch-range="[[patchRange]]"
path="[[file.__path]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 3257469..15f835d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -36,6 +36,7 @@
type: Object,
value: function() { return document.body; },
},
+ change: Object,
_files: {
type: Array,
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 0e99a15..903eabf 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
@@ -174,12 +174,16 @@
available-patches="[[_computeAvailablePatches(_change.revisions)]]">
</gr-patch-range-select>
<div>
- <select id="modeSelect" on-change="_handleModeChange">
+ <select
+ id="modeSelect"
+ on-change="_handleModeChange"
+ hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
<option value="SIDE_BY_SIDE">Side By Side</option>
<option value="UNIFIED_DIFF">Unified</option>
</select>
<span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
- /
+ <span
+ hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
<gr-button link
class="prefsButton"
on-tap="_handlePrefsTap">Preferences</gr-button>
@@ -193,6 +197,9 @@
on-cancel="_handlePrefsCancel"></gr-diff-preferences>
</gr-overlay>
<gr-diff id="diff"
+ project="[[_change.project]]"
+ commit="[[_change.current_revision]]"
+ is-image-diff="{{_isImageDiff}}"
change-num="[[_changeNum]]"
patch-range="[[_patchRange]]"
path="[[_path]]"
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 c75cb72..17aa0c4 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
@@ -74,6 +74,7 @@
type: String,
computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)'
},
+ _isImageDiff: Boolean,
},
behaviors: [
@@ -293,11 +294,11 @@
this._userPrefs = prefs;
}.bind(this)));
- promises.push(this.$.diff.reload());
+ promises.push(this._getChangeDetail(this._changeNum));
- Promise.all(promises).then(function() {
- this._loading = false;
- }.bind(this));
+ Promise.all(promises)
+ .then(function() { return this.$.diff.reload(); }.bind(this))
+ .then(function() { this._loading = false; }.bind(this));
},
_pathChanged: function(path) {
@@ -462,5 +463,9 @@
this.$.modeSelect.value = mode;
}
},
+
+ _computeModeSelectHidden: function() {
+ return this._isImageDiff;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
new file mode 100644
index 0000000..b897708
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
@@ -0,0 +1,100 @@
+// 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, GrDiffBuilderSideBySide) {
+ 'use strict';
+
+ function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
+ revisionImage) {
+ GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
+ this._baseImage = baseImage;
+ this._revisionImage = revisionImage;
+ }
+
+ GrDiffBuilderImage.prototype = Object.create(
+ GrDiffBuilderSideBySide.prototype);
+ GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
+
+ GrDiffBuilderImage.prototype.emitDiff = function() {
+ this.emitGroup(this._groups[0]);
+
+ var section = this._createElement('tbody', 'image-diff');
+
+ this._emitImagePair(section);
+ this._emitImageLabels(section);
+
+ this._outputEl.appendChild(section);
+ };
+
+ GrDiffBuilderImage.prototype._emitImagePair = function(section) {
+ var tr = this._createElement('tr');
+
+ tr.appendChild(this._createElement('td'));
+ tr.appendChild(this._createImageCell(this._baseImage, 'left'));
+
+ tr.appendChild(this._createElement('td'));
+ tr.appendChild(this._createImageCell(this._revisionImage, 'right'));
+
+ section.appendChild(tr);
+ };
+
+ GrDiffBuilderImage.prototype._createImageCell = function(image, className) {
+ var td = this._createElement('td', className);
+ if (image) {
+ var imageEl = this._createElement('img');
+ imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
+ image._height = imageEl.naturalHeight;
+ image._width = imageEl.naturalWidth;
+ imageEl.addEventListener('error', function(e) {
+ imageEl.remove();
+ td.textContent = '[Image failed to load]';
+ });
+ td.appendChild(imageEl);
+ }
+ return td;
+ };
+
+ GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
+ var tr = this._createElement('tr');
+
+ tr.appendChild(this._createElement('td'));
+ var td = this._createElement('td', 'left');
+ var label = this._createElement('label');
+ label.textContent = this._getImageLabel(this._baseImage);
+ td.appendChild(label);
+ tr.appendChild(td);
+
+ tr.appendChild(this._createElement('td'));
+ td = this._createElement('td', 'right');
+ label = this._createElement('label');
+ label.textContent = this._getImageLabel(this._revisionImage);
+ td.appendChild(label);
+ tr.appendChild(td);
+
+ section.appendChild(tr);
+ };
+
+ GrDiffBuilderImage.prototype._getImageLabel = function(image) {
+ if (image) {
+ var type = image.type || image._expectedType;
+ if (image._width && image._height) {
+ return image._width + '⨉' + image._height + ' ' + type;
+ } else {
+ return type;
+ }
+ }
+ return 'No image';
+ };
+
+ window.GrDiffBuilderImage = GrDiffBuilderImage;
+})(window, GrDiffBuilderSideBySide);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
index c2197eb..f55037c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -15,6 +15,7 @@
'use strict';
function GrDiffBuilder(diff, comments, prefs, outputEl) {
+ this._diff = diff;
this._comments = comments;
this._prefs = prefs;
this._outputEl = outputEl;
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 e4603b8..826a21e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -43,6 +43,17 @@
.section {
background-color: #eee;
}
+ .image-diff .gr-diff {
+ text-align: center;
+ }
+ .image-diff img {
+ max-width: 50em;
+ outline: 1px solid #ccc;
+ }
+ .image-diff label {
+ font-family: var(--font-family);
+ font-style: italic;
+ }
.diff-row.target-row.target-side-left .lineNum.left,
.diff-row.target-row.target-side-right .lineNum.right,
.diff-row.target-row.unified .lineNum {
@@ -151,5 +162,6 @@
<script src="gr-diff-builder.js"></script>
<script src="gr-diff-builder-side-by-side.js"></script>
<script src="gr-diff-builder-unified.js"></script>
+ <script src="gr-diff-builder-image.js"></script>
<script src="gr-diff.js"></script>
</dom-module>
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 660b86f..4cbe10f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -42,6 +42,13 @@
type: Object,
observer: '_projectConfigChanged',
},
+ project: String,
+ commit: String,
+ isImageDiff: {
+ type: Boolean,
+ computed: '_computeIsImageDiff(_diff)',
+ notify: true,
+ },
_loggedIn: {
type: Boolean,
@@ -82,6 +89,7 @@
promises.push(this._getDiff().then(function(diff) {
this._diff = diff;
+ return this._loadDiffAssets();
}.bind(this)));
promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
@@ -414,8 +422,40 @@
return this.$.restAPI.getLoggedIn();
},
+ _computeIsImageDiff: function() {
+ if (!this._diff) { return false; }
+
+ var isA = this._diff.meta_a &&
+ this._diff.meta_a.content_type.indexOf('image/') === 0;
+ var isB = this._diff.meta_b &&
+ this._diff.meta_b.content_type.indexOf('image/') === 0;
+
+ return this._diff.binary && (isA || isB);
+ },
+
+ _loadDiffAssets: function() {
+ if (this.isImageDiff) {
+ return this._getImages().then(function(images) {
+ this._baseImage = images.baseImage;
+ this._revisionImage = images.revisionImage;
+ }.bind(this));
+ } else {
+ this._baseImage = null;
+ this._revisionImage = null;
+ return Promise.resolve();
+ }
+ },
+
+ _getImages: function() {
+ return this.$.restAPI.getImagesForDiff(this.project, this.commit,
+ this.changeNum, this._diff, this.patchRange);
+ },
+
_getDiffBuilder: function(diff, comments, prefs) {
- if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+ if (this.isImageDiff) {
+ return new GrDiffBuilderImage(diff, comments, prefs, this.$.diffTable,
+ this._baseImage, this._revisionImage);
+ } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
return new GrDiffBuilderSideBySide(diff, comments, prefs,
this.$.diffTable);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
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 f02b861..cb091c9 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
@@ -20,6 +20,7 @@
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff.html">
@@ -242,5 +243,105 @@
],
}));
});
+
+ test('renders image diffs', function(done) {
+ var mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ var mockFile1 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsAAA' +
+ 'AAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ var mockFile2 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsAAA' +
+ 'AAAAAAAAAA/////w==',
+ type: 'image/bmp'
+ };
+ var mockCommit = {
+ commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
+ parents: [{
+ commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
+ subject: 'Added a carrot',
+ }],
+ author: {
+ name: 'Wyatt Allen',
+ email: 'wyatta@google.com',
+ date: '2016-05-23 21:44:51.000000000',
+ tz: -420,
+ },
+ committer: {
+ name: 'Wyatt Allen',
+ email: 'wyatta@google.com',
+ date: '2016-05-25 00:25:41.000000000',
+ tz: -420,
+ },
+ subject: 'Updated the carrot',
+ message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
+ };
+ var mockComments = {baseComments: [], comments: []};
+
+ var stubs = [];
+ stubs.push(sinon.stub(element, '_getDiff',
+ function() { return Promise.resolve(mockDiff); }));
+ stubs.push(sinon.stub(element.$.restAPI, 'getCommitInfo',
+ function() { return Promise.resolve(mockCommit); }));
+ stubs.push(sinon.stub(element.$.restAPI,
+ 'getCommitFileContents',
+ function() { return Promise.resolve(mockFile1); }));
+ stubs.push(sinon.stub(element.$.restAPI,
+ 'getChangeFileContents',
+ function() { return Promise.resolve(mockFile2); }));
+ stubs.push(sinon.stub(element.$.restAPI, '_getDiffComments',
+ function() { return Promise.resolve(mockComments); }));
+ stubs.push(sinon.stub(element.$.restAPI, 'getDiffDrafts',
+ function() { return Promise.resolve(mockComments); }));
+
+ element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+
+ var rendered = function() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(element._getDiffBuilder(element._diff,
+ element._comments, element.prefs), GrDiffBuilderImage);
+
+ // The left image rendered with the parent commit's version of the file.
+ var leftInmage = element.$.diffTable.querySelector('td.left img');
+ assert.isOk(leftInmage);
+ assert.equal(leftInmage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+
+ // The right image rendered with this change's revision of the image.
+ var rightInmage = element.$.diffTable.querySelector('td.right img');
+ assert.isOk(rightInmage);
+ assert.equal(rightInmage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+
+ // Cleanup.
+ element.removeEventListener('render', rendered);
+ stubs.forEach(function(stub) { stub.restore(); });
+
+ done();
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.$.restAPI.getDiffPreferences().then(function(prefs) {
+ element.prefs = prefs;
+ element.reload();
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 10c8a29..72ae4e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -18,7 +18,6 @@
<script src="../../../bower_components/fetch/fetch.js"></script>
<dom-module id="gr-rest-api-interface">
- <template></template>
<script src="gr-rest-api-interface.js"></script>
</dom-module>
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 e681bf9..9604ded 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
@@ -682,5 +682,84 @@
return '';
},
+ getCommitInfo: function(project, commit) {
+ return this.fetchJSON(
+ '/projects/' + encodeURIComponent(project) +
+ '/commits/' + encodeURIComponent(commit));
+ },
+
+ _fetchB64File: function(url) {
+ return fetch(url).then(function(response) {
+ var type = response.headers.get('X-FYI-Content-Type');
+ return response.text()
+ .then(function(text) {
+ return {body: text, type: type};
+ });
+ });
+ },
+
+ getChangeFileContents: function(changeId, patchNum, path) {
+ return this._fetchB64File(
+ '/changes/' + encodeURIComponent(changeId) +
+ '/revisions/' + encodeURIComponent(patchNum) +
+ '/files/' + encodeURIComponent(path) +
+ '/content');
+ },
+
+ getCommitFileContents: function(projectName, commit, path) {
+ return this._fetchB64File(
+ '/projects/' + encodeURIComponent(projectName) +
+ '/commits/' + encodeURIComponent(commit) +
+ '/files/' + encodeURIComponent(path) +
+ '/content');
+ },
+
+ getImagesForDiff: function(project, commit, changeNum, diff, patchRange) {
+ var promiseA;
+ var promiseB;
+
+ if (diff.meta_a && diff.meta_a.content_type.indexOf('image/') === 0) {
+ if (patchRange.basePatchNum === 'PARENT') {
+ // Need the commit info know the parent SHA.
+ promiseA = this.getCommitInfo(project, commit).then(function(info) {
+ if (info.parents.length !== 1) {
+ return Promise.reject('Change commit has multiple parents.');
+ }
+ var parent = info.parents[0].commit;
+ return this.getCommitFileContents(project, parent,
+ diff.meta_a.name);
+ }.bind(this));
+
+ } else {
+ promiseA = this.getChangeFileContents(changeNum,
+ patchRange.basePatchNum, diff.meta_a.name);
+ }
+ } else {
+ promiseA = Promise.resolve(null);
+ }
+
+ if (diff.meta_b && diff.meta_b.content_type.indexOf('image/') === 0) {
+ promiseB = this.getChangeFileContents(changeNum, patchRange.patchNum,
+ diff.meta_b.name);
+ } else {
+ promiseB = Promise.resolve(null);
+ }
+
+ return Promise.all([promiseA, promiseB])
+ .then(function(results) {
+ var baseImage = results[0];
+ var revisionImage = results[1];
+
+ // Sometimes the server doesn't send back the content type.
+ if (baseImage) {
+ baseImage._expectedType = diff.meta_a.content_type;
+ }
+ if (revisionImage) {
+ revisionImage._expectedType = diff.meta_b.content_type;
+ }
+
+ return {baseImage: baseImage, revisionImage: revisionImage};
+ }.bind(this));
+ },
});
})();