Merge "Add optional file link to gr-comment-thread"
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 5dacd71..072c22c 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -121,11 +121,24 @@
]
----
+If the plugin(s) being bundled in the release have external dependencies, include them
+in `plugins/external_plugin_deps`. You should alias `external_plugin_deps()` so it
+can be imported for multiple plugins. For example:
+
+----
+load(":my-plugin/external_plugin_deps.bzl", my_plugin="external_plugin_deps")
+load(":my-other-plugin/external_plugin_deps.bzl", my_other_plugin="external_plugin_deps")
+
+def external_plugin_deps():
+ my_plugin()
+ my_other_plugin()
+----
+
[NOTE]
-Since `tools/bzl/plugins.bzl` is part of Gerrit's source code and the version
-of the war is based on the state of the git repository that is built; you should
-commit this change before building, otherwise the version will be marked as
-'dirty'.
+Since `tools/bzl/plugins.bzl` and `plugins/external_plugin_deps.bzl` are part of
+Gerrit's source code and the version of the war is based on the state of the git
+repository that is built; you should commit this change before building, otherwise
+the version will be marked as 'dirty'.
== Bazel standalone driven
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 822841c..2958888 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -14,7 +14,21 @@
package com.google.gerrit.acceptance.rest.change;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.testing.Util;
+import java.util.List;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
import org.junit.Test;
public class IndexChangeIT extends AbstractDaemonTest {
@@ -30,4 +44,62 @@
blockRead("refs/heads/master");
userRestSession.post("/changes/" + changeId + "/index/").assertNotFound();
}
+
+ @Test
+ public void indexChangeAfterOwnerLosesVisibility() throws Exception {
+ // Create a test group with 2 users as members
+ TestAccount user2 = accountCreator.user2();
+ String group = createGroup("test");
+ gApi.groups().id(group).addMembers("admin", "user", user2.username);
+
+ // Create a project and restrict its visibility to the group
+ Project.NameKey p = createProject("p");
+ ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+ Util.allow(
+ cfg,
+ Permission.READ,
+ groupCache.get(new AccountGroup.NameKey(group)).get().getGroupUUID(),
+ "refs/*");
+ Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+ saveProjectConfig(p, cfg);
+
+ // Clone it and push a change as a regular user
+ TestRepository<InMemoryRepository> repo = cloneProject(p, user);
+ PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ result.assertOkStatus();
+ assertThat(result.getChange().change().getOwner()).isEqualTo(user.id);
+ String changeId = result.getChangeId();
+
+ // User can see the change and it is mergeable
+ setApiUser(user);
+ List<ChangeInfo> changes = gApi.changes().query(changeId).get();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).mergeable).isNotNull();
+
+ // Other user can see the change and it is mergeable
+ setApiUser(user2);
+ changes = gApi.changes().query(changeId).get();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).mergeable).isTrue();
+
+ // Remove the user from the group so they can no longer see the project
+ setApiUser(admin);
+ gApi.groups().id(group).removeMembers("user");
+
+ // User can no longer see the change
+ setApiUser(user);
+ changes = gApi.changes().query(changeId).get();
+ assertThat(changes).isEmpty();
+
+ // Reindex the change
+ setApiUser(admin);
+ gApi.changes().id(changeId).index();
+
+ // Other user can still see the change and it is still mergeable
+ setApiUser(user2);
+ changes = gApi.changes().query(changeId).get();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).mergeable).isTrue();
+ }
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index f1667d3..45109cd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -23,6 +23,7 @@
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
+<link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -69,6 +70,17 @@
height: 0;
overflow: hidden;
}
+ .status {
+ align-items: center;
+ display: inline-flex;
+ }
+ .status .comma {
+ padding-right: .2rem;
+ }
+ /* Used to hide the leading separator comma for statuses. */
+ .status .comma:first-of-type {
+ display: none;
+ }
a {
color: var(--default-text-color);
cursor: pointer;
@@ -81,6 +93,9 @@
.positionIndicator {
visibility: hidden;
}
+ .size {
+ text-align: center;
+ }
:host([selected]) .positionIndicator {
visibility: visible;
}
@@ -100,6 +115,7 @@
.u-gray-background {
background-color: #F5F5F5;
}
+ .comma,
.placeholder {
color: rgba(0, 0, 0, .87);
}
@@ -136,21 +152,26 @@
</td>
<td class="cell status"
hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
- <template is="dom-if" if="[[status]]">
- [[status]]
+ <template is="dom-repeat" items="[[statuses]]" as="status">
+ <div class="comma">,</div>
+ <gr-change-status flat status="[[status]]"></gr-change-status>
</template>
- <template is="dom-if" if="[[!status]]">
+ <template is="dom-if" if="[[!statuses.length]]">
<span class="placeholder">--</span>
</template>
</td>
<td class="cell owner"
hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
- <gr-account-link account="[[change.owner]]"></gr-account-link>
+ <gr-account-link
+ account="[[change.owner]]"
+ additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
</td>
<td class="cell assignee"
hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
<template is="dom-if" if="[[change.assignee]]">
- <gr-account-link account="[[change.assignee]]"></gr-account-link>
+ <gr-account-link
+ account="[[change.assignee]]"
+ additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
</template>
<template is="dom-if" if="[[!change.assignee]]">
<span class="placeholder">--</span>
@@ -183,10 +204,10 @@
has-tooltip
date-str="[[change.updated]]"></gr-date-formatter>
</td>
- <td class="cell size u-monospace"
+ <td class="cell size"
+ title$="[[_computeSizeTooltip(change)]]"
hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]">
- <span class="u-green"><span>+</span>[[change.insertions]]</span>,
- <span class="u-red"><span>-</span>[[change.deletions]]</span>
+ <span>[[_computeChangeSize(change)]]</span>
</td>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
<td title$="[[_computeLabelTitle(change, labelName)]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 0735e2c..5d7121a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -14,6 +14,13 @@
(function() {
'use strict';
+ const CHANGE_SIZE = {
+ XS: 10,
+ SMALL: 50,
+ MEDIUM: 250,
+ LARGE: 1000,
+ };
+
Polymer({
is: 'gr-change-list-item',
@@ -29,9 +36,9 @@
type: String,
computed: '_computeChangeURL(change)',
},
- status: {
- type: String,
- computed: 'changeStatusString(change)',
+ statuses: {
+ type: Array,
+ computed: 'changeStatuses(change)',
},
showStar: {
type: Boolean,
@@ -125,5 +132,40 @@
if (!project) { return ''; }
return this.truncatePath(project, 2);
},
+
+ _computeAccountStatusString(account) {
+ return account && account.status ? `(${account.status})` : '';
+ },
+
+ _computeSizeTooltip(change) {
+ if (change.insertions + change.deletions === 0 ||
+ isNaN(change.insertions + change.deletions)) {
+ return 'Size unknown';
+ } else {
+ return `+${change.insertions}, -${change.deletions}`;
+ }
+ },
+
+ /**
+ * TShirt sizing is based on the following paper:
+ * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
+ */
+ _computeChangeSize(change) {
+ const delta = change.insertions + change.deletions;
+ if (isNaN(delta) || delta === 0) {
+ return '🤷'; // Unknown
+ }
+ if (delta < CHANGE_SIZE.XS) {
+ return 'XS';
+ } else if (delta < CHANGE_SIZE.SMALL) {
+ return 'S';
+ } else if (delta < CHANGE_SIZE.MEDIUM) {
+ return 'M';
+ } else if (delta < CHANGE_SIZE.LARGE) {
+ return 'L';
+ } else {
+ return 'XL';
+ }
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 6f2e6da..4b001c3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -200,5 +200,53 @@
flushAsynchronousOperations();
assert.isOk(element.$$('.assignee gr-account-link'));
});
+
+ test('_computeAccountStatusString', () => {
+ assert.equal(element._computeAccountStatusString({}), '');
+ assert.equal(element._computeAccountStatusString({status: 'Working'}),
+ '(Working)');
+ });
+
+ test('TShirt sizing tooltip', () => {
+ assert.equal(element._computeSizeTooltip({
+ insertions: 'foo',
+ deletions: 'bar',
+ }), 'Size unknown');
+ assert.equal(element._computeSizeTooltip({
+ insertions: 0,
+ deletions: 0,
+ }), 'Size unknown');
+ assert.equal(element._computeSizeTooltip({
+ insertions: 1,
+ deletions: 2,
+ }), '+1, -2');
+ });
+
+ test('TShirt sizing', () => {
+ assert.equal(element._computeChangeSize({
+ insertions: 'foo',
+ deletions: 'bar',
+ }), '🤷');
+ assert.equal(element._computeChangeSize({
+ insertions: 1,
+ deletions: 1,
+ }), 'XS');
+ assert.equal(element._computeChangeSize({
+ insertions: 9,
+ deletions: 1,
+ }), 'S');
+ assert.equal(element._computeChangeSize({
+ insertions: 10,
+ deletions: 200,
+ }), 'M');
+ assert.equal(element._computeChangeSize({
+ insertions: 99,
+ deletions: 900,
+ }), 'L');
+ assert.equal(element._computeChangeSize({
+ insertions: 99,
+ deletions: 999,
+ }), 'XL');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index a954559..adab1c8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -211,6 +211,10 @@
type: Boolean,
value: false,
},
+ _hideQuickApproveAction: {
+ type: Boolean,
+ value: false,
+ },
changeNum: String,
changeStatus: String,
commitNum: String,
@@ -653,7 +657,18 @@
return null;
},
+ hideQuickApproveAction() {
+ this._topLevelSecondaryActions =
+ this._topLevelSecondaryActions.filter(sa => {
+ return sa.key !== QUICK_APPROVE_ACTION.key;
+ });
+ this._hideQuickApproveAction = true;
+ },
+
_getQuickApproveAction() {
+ if (this._hideQuickApproveAction) {
+ return null;
+ }
const approval = this._getTopMissingApproval();
if (!approval) {
return null;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 8986816..835d560 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -1091,6 +1091,21 @@
assert.isNotNull(approveButton);
});
+ test('hide quick approve', () => {
+ const approveButton =
+ element.$$('gr-button[data-action-key=\'review\']');
+ assert.isNotNull(approveButton);
+ assert.isFalse(element._hideQuickApproveAction);
+
+ // Assert approve button gets removed from list of buttons.
+ element.hideQuickApproveAction();
+ flushAsynchronousOperations();
+ const approveButtonUpdated =
+ element.$$('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButtonUpdated);
+ assert.isTrue(element._hideQuickApproveAction);
+ });
+
test('is first in list of secondary actions', () => {
const approveButton = element.$.secondaryActions
.querySelector('gr-button');
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 6ce6a1e..464e1bb 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -42,7 +42,7 @@
}
.container {
display: flex;
- margin: 5px 0;
+ margin: .5em 0;
}
.lineNum {
margin-right: .5em;
@@ -53,6 +53,16 @@
flex: 1;
--gr-formatted-text-prose-max-width: 80ch;
}
+ @media screen and (max-width: 50em) {
+ .container {
+ flex-direction: column;
+ margin: 0 0 .5em .5em;
+ }
+ .lineNum {
+ min-width: initial;
+ text-align: left;
+ }
+ }
</style>
<template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
<div class="file">[[computeDisplayPath(file)]]:</div>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 4370d7e..ea70de9 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -95,7 +95,7 @@
type="radio"
on-tap="_handleRebaseOnOther">
<label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
- Rebase on a specific change or ref <span hidden$="[[!hasParent]]">
+ Rebase on a specific change, ref, or commit <span hidden$="[[!hasParent]]">
(breaks relation chain)
</span>
</label>
@@ -107,7 +107,8 @@
text="{{_inputText}}"
on-tap="_handleEnterChangeNumberTap"
on-commit="_handleBaseSelected"
- placeholder="Change number">
+ allow-non-suggested-values
+ placeholder="Change number, ref, or commit hash">
</gr-autocomplete>
</div>
</div>
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 a326f54..0e7a709 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
@@ -14,8 +14,6 @@
(function() {
'use strict';
- const ERR_EDIT_LOADED = 'You cannot change the review status of an edit.';
-
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
const WARN_SHOW_ALL_THRESHOLD = 1000;
@@ -397,10 +395,7 @@
},
_reviewFile(path) {
- if (this.editMode) {
- this.fire('show-alert', {message: ERR_EDIT_LOADED});
- return;
- }
+ if (this.editMode) { return; }
const index = this._reviewed.indexOf(path);
const reviewed = index !== -1;
if (reviewed) {
@@ -896,7 +891,7 @@
diffElem.comments = this.changeComments.getCommentsBySideForPath(
path, this.patchRange, this.projectConfig);
const promises = [diffElem.reload()];
- if (this._isLoggedIn) {
+ if (this._loggedIn && !this.diffPrefs.manual_review) {
promises.push(this._reviewFile(path));
}
return Promise.all(promises);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 38ab31b..72a9629 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -85,7 +85,7 @@
.returns({meta: {}, left: [], right: []});
done();
});
-
+ element.diffPrefs = {};
element.numFilesShown = 200;
saveStub = sandbox.stub(element, '_saveReviewedState',
() => { return Promise.resolve(); });
@@ -927,7 +927,7 @@
});
test('_renderInOrder logged in', done => {
- element._isLoggedIn = true;
+ element._loggedIn = true;
const reviewStub = sandbox.stub(element, '_reviewFile');
let callCount = 0;
const diffs = [{
@@ -959,6 +959,24 @@
});
});
+ test('_renderInOrder respects diffPrefs.manual_review', () => {
+ element._loggedIn = true;
+ element.diffPrefs = {manual_review: true};
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ const diffs = [{
+ path: 'p',
+ reload() { return Promise.resolve(); },
+ }];
+
+ return element._renderInOrder(['p'], diffs, 1).then(() => {
+ assert.isFalse(reviewStub.called);
+ delete element.diffPrefs.manual_review;
+ return element._renderInOrder(['p'], diffs, 1).then(() => {
+ assert.isTrue(reviewStub.called);
+ });
+ });
+ });
+
test('_loadingChanged fired from reload in debouncer', done => {
element.changeNum = 123;
element.patchRange = {patchNum: 12};
@@ -1102,6 +1120,8 @@
commentApiWrapper = fixture('basic');
element = commentApiWrapper.$.fileList;
loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+ element.diffPrefs = {};
+ sandbox.stub(element, '_reviewFile');
// Stub methods on the changeComments object after changeComments has
// been initalized.
@@ -1323,20 +1343,17 @@
suite('editMode behavior', () => {
test('reviewed checkbox', () => {
- const alertStub = sandbox.stub();
+ element._reviewFile.restore();
const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
- element.addEventListener('show-alert', alertStub);
element.editMode = false;
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isFalse(alertStub.called);
assert.isTrue(saveReviewStub.calledOnce);
element.editMode = true;
flushAsynchronousOperations();
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(alertStub.called);
assert.isTrue(saveReviewStub.calledOnce);
});
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index b1e6a5a..348eabb 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -130,13 +130,13 @@
},
_computeReviewerTooltip(reviewer, change) {
- if (!change || !change.permitted_labels) return '';
+ if (!change || !change.labels) { return ''; }
const maxScores = [];
const maxPermitted = this._getMaxPermittedScores(change);
- for (const label of Object.keys(change.permitted_labels)) {
+ for (const label of Object.keys(change.labels)) {
const maxScore =
this._getReviewerPermittedScore(reviewer, change, label);
- if (isNaN(maxScore) || maxScore < 0) continue;
+ if (isNaN(maxScore) || maxScore < 0) { continue; }
if (maxScore > 0 && maxScore === maxPermitted[label]) {
maxScores.push(`${label}: +${maxScore}`);
} else {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 985e4bb..24fc4d1 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -307,7 +307,6 @@
},
permitted_labels: {
Foo: ['-1', ' 0', '+1', '+2'],
- Bar: ['-1', ' 0', '+1', '+2'],
FooBar: ['-1', ' 0'],
},
};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 1dcfc68..ccc5361 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -120,11 +120,6 @@
this.$.prefsOverlay.close();
},
- _handlePrefsTap(e) {
- e.preventDefault();
- this._openPrefs();
- },
-
open() {
this.$.prefsOverlay.open().then(() => {
const focusStops = this.getFocusStops();
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 794e1fb..36ae32a 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
@@ -287,6 +287,12 @@
on-tap="_handlePrefsTap">Preferences</gr-button>
</span>
</span>
+ <gr-endpoint-decorator name="annotation-toggler">
+ <span hidden id="annotation-span">
+ <label for="annotation-checkbox" id="annotation-label"></label>
+ <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+ </span>
+ </gr-endpoint-decorator>
<span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _isBlameSupported)]]">
<span class="separator"></span>
<gr-button
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 4fedf73..bd07375 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -135,7 +135,8 @@
_viewEditInChangeView() {
const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
- Gerrit.Nav.navigateToChange(this._change, patch);
+ Gerrit.Nav.navigateToChange(this._change, patch, null,
+ patch !== this.EDIT_NAME);
},
_getFileData(changeNum, path, patchNum) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
new file mode 100644
index 0000000..b38509d
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -0,0 +1,137 @@
+<!--
+Copyright (C) 2017 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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-gpg-editor">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
+ .statusHeader {
+ width: 4em;
+ }
+ .keyHeader {
+ width: 9em;
+ }
+ .userIdHeader {
+ width: 15em;
+ }
+ #viewKeyOverlay {
+ padding: 2em;
+ width: 50em;
+ }
+ .publicKey {
+ font-family: var(--monospace-font-family);
+ overflow-x: scroll;
+ overflow-wrap: break-word;
+ width: 30em;
+ }
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ #existing {
+ margin-bottom: 1em;
+ }
+ #existing .commentColumn {
+ min-width: 27em;
+ width: auto;
+ }
+ </style>
+ <div class="gr-form-styles">
+ <fieldset id="existing">
+ <table>
+ <thead>
+ <tr>
+ <th class="idColumn">ID</th>
+ <th class="fingerPrintColumn">Fingerprint</th>
+ <th class="userIdHeader">User IDs</th>
+ <th class="keyHeader">Public Key</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template is="dom-repeat" items="[[_keys]]" as="key">
+ <tr>
+ <td class="idColumn">[[key.id]]</td>
+ <td class="fingerPrintColumn">[[key.fingerprint]]</td>
+ <td class="userIdHeader">
+ <template is="dom-repeat" items="[[key.user_ids]]">
+ [[item]]
+ </template>
+ </td>
+ <td class="keyHeader">
+ <gr-button
+ on-tap="_showKey"
+ data-index$="[[index]]"
+ link>Click to View</gr-button>
+ </td>
+ <td>
+ <gr-button
+ data-index$="[[index]]"
+ on-tap="_handleDeleteKey">Delete</gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <gr-overlay id="viewKeyOverlay" with-backdrop>
+ <fieldset>
+ <section>
+ <span class="title">Status</span>
+ <span class="value">[[_keyToView.status]]</span>
+ </section>
+ <section>
+ <span class="title">Key</span>
+ <span class="value">[[_keyToView.key]]</span>
+ </section>
+ </fieldset>
+ <gr-button
+ class="closeButton"
+ on-tap="_closeOverlay">Close</gr-button>
+ </gr-overlay>
+ <gr-button
+ on-tap="save"
+ disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">New GPG key</span>
+ <span class="value">
+ <iron-autogrow-textarea
+ id="newKey"
+ autocomplete="on"
+ bind-value="{{_newKey}}"
+ placeholder="New GPG Key"></iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button
+ id="addButton"
+ disabled$="[[_computeAddButtonDisabled(_newKey)]]"
+ on-tap="_handleAddKey">Add new GPG key</gr-button>
+ </fieldset>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-gpg-editor.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
new file mode 100644
index 0000000..f5bf8bc
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2017 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() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-gpg-editor',
+
+ properties: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ },
+
+ loadData() {
+ this._keys = [];
+ return this.$.restAPI.getAccountGPGKeys().then(keys => {
+ if (!keys) {
+ return;
+ }
+ this._keys = Object.keys(keys)
+ .map(key => {
+ const gpgKey = keys[key];
+ gpgKey.id = key;
+ return gpgKey;
+ });
+ });
+ },
+
+ save() {
+ const promises = this._keysToRemove.map(key => {
+ this.$.restAPI.deleteAccountGPGKey(key.id);
+ });
+
+ return Promise.all(promises).then(() => {
+ this._keysToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ },
+
+ _showKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ },
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ },
+
+ _handleDeleteKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+ this.splice('_keys', index, 1);
+ this.hasUnsavedChanges = true;
+ },
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
+ .then(key => {
+ this.$.newKey.disabled = false;
+ this._newKey = '';
+ this.loadData();
+ }).catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ },
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
new file mode 100644
index 0000000..f749130
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-gpg-editor</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-gpg-editor.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-gpg-editor></gr-gpg-editor>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-gpg-editor tests', () => {
+ let element;
+ let keys;
+
+ setup(done => {
+ const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ keys = {
+ AFC8A49B: {
+ fingerprint: fingerprint1,
+ user_ids: [
+ 'John Doe john.doe@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 1>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ AED9B59C: {
+ fingerprint: fingerprint2,
+ user_ids: [
+ 'Gerrit gerrit@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 2>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
+
+ stub('gr-rest-api-interface', {
+ getAccountGPGKeys() { return Promise.resolve(keys); },
+ });
+
+ element = fixture('basic');
+
+ element.loadData().then(() => { flush(done); });
+ });
+
+ test('renders', () => {
+ const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+
+ assert.equal(rows.length, 2);
+
+ let cells = rows[0].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AFC8A49B');
+
+ cells = rows[1].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AED9B59C');
+ });
+
+ test('remove key', done => {
+ const lastKey = keys[Object.keys(keys)[1]];
+
+ const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
+ () => { return Promise.resolve(); });
+
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ // Get the delete button for the last row.
+ const button = Polymer.dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(5) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keys.length, 1);
+ assert.equal(element._keysToRemove.length, 1);
+ assert.equal(element._keysToRemove[0], lastKey);
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isFalse(saveStub.called);
+
+ element.save().then(() => {
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+ done();
+ });
+ });
+
+ test('show key', () => {
+ const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+ // Get the show button for the last row.
+ const button = Polymer.dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(4) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
+ assert.isTrue(openSpy.called);
+ });
+
+ test('add key', done => {
+ const newKeyString =
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 3>';
+ const newKeyObject = {
+ ADE8A59B: {
+ fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B',
+ user_ids: [
+ 'John john@example.com',
+ ],
+ key: newKeyString,
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => { return Promise.resolve(newKeyObject); });
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+
+ test('add invalid key', done => {
+ const newKeyString = 'not even close to valid';
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => { return Promise.reject(); });
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index a1de1b2..416c426 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -31,6 +31,7 @@
<link rel="import" href="../gr-agreements-list/gr-agreements-list.html">
<link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html">
<link rel="import" href="../gr-email-editor/gr-email-editor.html">
+<link rel="import" href="../gr-gpg-editor/gr-gpg-editor.html">
<link rel="import" href="../gr-group-list/gr-group-list.html">
<link rel="import" href="../gr-http-password/gr-http-password.html">
<link rel="import" href="../gr-identities/gr-identities.html">
@@ -73,6 +74,9 @@
<li hidden$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
SSH Keys
</a></li>
+ <li hidden$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
+ GPG Keys
+ </a></li>
<li><a href="#Groups">Groups</a></li>
<li><a href="#Identities">Identities</a></li>
<template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
@@ -414,6 +418,14 @@
id="sshEditor"
has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
</div>
+ <div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
+ <h2
+ id="GPGKeys"
+ class$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
+ <gr-gpg-editor
+ id="gpgEditor"
+ has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
+ </div>
<h2 id="Groups">Groups</h2>
<fieldset>
<gr-group-list id="groupList"></gr-group-list>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 8e14018..912712c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -104,6 +104,10 @@
type: Boolean,
value: false,
},
+ _gpgKeysChanged: {
+ type: Boolean,
+ value: false,
+ },
_newEmail: String,
_addingEmail: {
type: Boolean,
@@ -167,10 +171,16 @@
this._serverConfig = config;
const configPromises = [];
- if (this._serverConfig.sshd) {
+ if (this._serverConfig && this._serverConfig.sshd) {
configPromises.push(this.$.sshEditor.loadData());
}
+ if (this._serverConfig &&
+ this._serverConfig.receive &&
+ this._serverConfig.receive.enable_signed_push) {
+ configPromises.push(this.$.gpgEditor.loadData());
+ }
+
configPromises.push(
this.getDocsBaseUrl(config, this.$.restAPI)
.then(baseUrl => { this._docsBaseUrl = baseUrl; }));
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
index cdd5414..eab0173 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
@@ -26,7 +26,6 @@
.chip {
border-radius: 4px;
background-color: var(--chip-background-color);
- color: #fff;
font-family: var(--font-family);
font-size: var(--font-size-normal);
padding: .1em .5em;
@@ -34,27 +33,42 @@
}
:host(.merged) .chip {
background-color: #5b9d52;
+ color: #5b9d52;
}
:host(.abandoned) .chip {
background-color: #afafaf;
+ color: #afafaf;
}
:host(.wip) .chip {
background-color: #8f756c;
+ color: #8f756c;
}
:host(.private) .chip {
background-color: #c17ccf;
+ color: #c17ccf;
}
:host(.merge-conflict) .chip {
background-color: #dc5c60;
+ color: #dc5c60;
}
:host(.active) .chip {
background-color: #29b6f6;
+ color: #29b6f6;
}
:host(.ready-to-submit) .chip {
background-color: #e10ca3;
+ color: #e10ca3;
}
:host(.custom) .chip {
background-color: #825cc2;
+ color: #825cc2;
+ }
+ :host([flat]) .chip {
+ background-color: transparent;
+ padding: .1em;
+ }
+ :host:not([flat]) .chip {
+ color: white;
}
</style>
<gr-tooltip-content
@@ -62,8 +76,11 @@
position-below
title="[[tooltipText]]"
max-width="40em">
- <div class="chip" aria-label$="Label: [[status]]">
- [[_computeStatusString(status)]]</div>
+ <div
+ class="chip"
+ aria-label$="Label: [[status]]">
+ [[_computeStatusString(status)]]
+ </div>
</gr-tooltip-content>
</template>
<script src="gr-change-status.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 991fea3..cd27a28 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -33,6 +33,11 @@
is: 'gr-change-status',
properties: {
+ flat: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
status: {
type: String,
observer: '_updateChipDetails',
@@ -44,7 +49,7 @@
},
_computeStatusString(status) {
- if (status === ChangeStates.WIP) {
+ if (status === ChangeStates.WIP && !this.flat) {
return 'Work in Progress';
}
return status;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
index 801249d..212296f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -52,6 +52,15 @@
assert.isTrue(element.classList.contains('wip'));
});
+ test('WIP flat', () => {
+ element.flat = true;
+ element.status = 'WIP';
+ assert.equal(element.$$('.chip').innerText, 'WIP');
+ assert.isDefined(element.tooltipText);
+ assert.isTrue(element.classList.contains('wip'));
+ assert.isTrue(element.hasAttribute('flat'));
+ });
+
test('merged', () => {
element.status = 'Merged';
assert.equal(element.$$('.chip').innerText, element.status);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
index 94bae45..6350c54 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
@@ -52,6 +52,45 @@
};
/**
+ * Returns a checkbox HTMLElement that can be used to toggle annotations
+ * on/off. The checkbox will be initially disabled. Plugins should enable it
+ * when data is ready and should add a click handler to toggle CSS on/off.
+ *
+ * Note1: Calling this method from multiple plugins will only work for the
+ * 1st call. It will print an error message for all subsequent calls
+ * and will not invoke their onAttached functions.
+ * Note2: This method will be deprecated and eventually removed when
+ * https://bugs.chromium.org/p/gerrit/issues/detail?id=8077 is
+ * implemented.
+ *
+ * @param {String} checkboxLabel Will be used as the label for the checkbox.
+ * Optional. "Enable" is used if this is not specified.
+ * @param {Function<HTMLElement>} onAttached The function that will be called
+ * when the checkbox is attached to the page.
+ */
+ GrAnnotationActionsInterface.prototype.enableToggleCheckbox = function(
+ checkboxLabel, onAttached) {
+ this.plugin.hook('annotation-toggler').onAttached(element => {
+ if (!element.content.hidden) {
+ console.error(
+ element.content.id + ' is already enabled. Cannot re-enable.');
+ return;
+ }
+ element.content.removeAttribute('hidden');
+
+ const label = element.content.querySelector('#annotation-label');
+ if (checkboxLabel) {
+ label.textContent = checkboxLabel;
+ } else {
+ label.textContent = 'Enable';
+ }
+ const checkbox = element.content.querySelector('#annotation-checkbox');
+ onAttached(checkbox);
+ });
+ return this;
+ };
+
+ /**
* The notify function will call the listeners of all required annotation
* layers. Intended to be called by the plugin when all required data for
* annotation is available.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
index 39623ed..a19df85 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -23,14 +23,23 @@
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
+<test-fixture id="basic">
+ <template>
+ <span hidden id="annotation-span">
+ <label for="annotation-checkbox" id="annotation-label"></label>
+ <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+ </span>
+ </template>
+</test-fixture>
+
<script>
suite('gr-annotation-actions-js-api tests', () => {
let annotationActions;
let sandbox;
+ let plugin;
setup(() => {
sandbox = sinon.sandbox.create();
- let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
annotationActions = plugin.annotationApi();
@@ -101,6 +110,45 @@
assert.isTrue(layer2Spy.called);
});
+ test('toggle checkbox', () => {
+ fakeEl = {content: fixture('basic')};
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+
+ let checkbox;
+ let onAttachedFuncCalled = false;
+ const onAttachedFunc = c => {
+ checkbox = c;
+ onAttachedFuncCalled = true;
+ };
+ annotationActions.enableToggleCheckbox('test label', onAttachedFunc);
+ emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
+ emulateAttached();
+
+ // Assert that onAttachedFunc is called and HTML elements have the
+ // expected state.
+ assert.isTrue(onAttachedFuncCalled);
+ assert.equal(checkbox.id, 'annotation-checkbox');
+ assert.isTrue(checkbox.disabled);
+ assert.equal(document.getElementById('annotation-label').textContent,
+ 'test label');
+ assert.isFalse(document.getElementById('annotation-span').hidden);
+
+ // Assert that error is shown if we try to enable checkbox again.
+ onAttachedFuncCalled = false;
+ annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
+ const errorStub = sandbox.stub(
+ console, 'error', (msg, err) => undefined);
+ emulateAttached();
+ assert.isTrue(
+ errorStub.calledWith(
+ 'annotation-span is already enabled. Cannot re-enable.'));
+ // Assert that onAttachedFunc is not called and the label has not changed.
+ assert.isFalse(onAttachedFuncCalled);
+ assert.equal(document.getElementById('annotation-label').textContent,
+ 'test label');
+ });
+
test('layer notify listeners', () => {
const annotationLayer = annotationActions.getLayer(
'/dummy/path', 1, 2);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index fe74906..7be007f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -62,6 +62,11 @@
});
};
+ GrChangeActionsInterface.prototype.hideQuickApproveAction = function() {
+ ensureEl(this);
+ this._el.hideQuickApproveAction();
+ };
+
GrChangeActionsInterface.prototype.setActionOverflow = function(type, key,
overflow) {
ensureEl(this);
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 bcc16ad..27f330e 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
@@ -1952,6 +1952,28 @@
return this.send('DELETE', '/accounts/self/sshkeys/' + id);
},
+ getAccountGPGKeys() {
+ return this.fetchJSON('/accounts/self/gpgkeys');
+ },
+
+ addAccountGPGKey(key) {
+ return this.send('POST', '/accounts/self/gpgkeys', key)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject();
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj) { return Promise.reject(); }
+ return obj;
+ });
+ },
+
+ deleteAccountGPGKey(id) {
+ return this.send('DELETE', '/accounts/self/gpgkeys/' + id);
+ },
+
deleteVote(changeNum, account, label) {
const e = `/reviewers/${account}/votes/${encodeURIComponent(label)}`;
return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
index 6f76dc4..9bec658 100644
--- a/polygerrit-ui/app/samples/coverage-plugin.html
+++ b/polygerrit-ui/app/samples/coverage-plugin.html
@@ -20,35 +20,53 @@
changeNum: 77001,
patchNum: 1,
};
+ coverageData['go/sklog/sklog.go'] = {
+ linesMissingCoverage: [3, 322, 323, 324],
+ totalLines: 350,
+ changeNum: 85963,
+ patchNum: 13,
+ };
}
Gerrit.install(plugin => {
const coverageData = {};
- plugin.annotationApi().addNotifier(notifyFunc => {
- new Promise(resolve => setTimeout(resolve, 3000)).then(
- () => {
- populateWithDummyData(coverageData);
- Object.keys(coverageData).forEach(file => {
- notifyFunc(file, 0, coverageData[file].totalLines, 'right');
- });
- });
- }).addLayer(context => {
+ let displayCoverage = false;
+ const annotationApi = plugin.annotationApi();
+ annotationApi.addLayer(context => {
if (Object.keys(coverageData).length === 0) {
- // Coverage data is not ready yet.
+ // Coverage data is not ready yet.
return;
}
const path = context.path;
const line = context.line;
- // Highlight lines missing coverage with this background color.
- const cssClass = Gerrit.css('background-color: #EF9B9B');
+ // Highlight lines missing coverage with this background color if
+ // coverage should be displayed, else do nothing.
+ const cssClass = displayCoverage
+ ? Gerrit.css('background-color: #EF9B9B')
+ : Gerrit.css('');
if (coverageData[path] &&
- coverageData[path].changeNum === context.changeNum &&
- coverageData[path].patchNum === context.patchNum) {
+ coverageData[path].changeNum === context.changeNum &&
+ coverageData[path].patchNum === context.patchNum) {
const linesMissingCoverage = coverageData[path].linesMissingCoverage;
if (linesMissingCoverage.includes(line.afterNumber)) {
context.annotateRange(0, line.text.length, cssClass, 'right');
}
}
+ }).enableToggleCheckbox('Display Coverage', checkbox => {
+ // Checkbox is attached so now add the notifier that will be controlled
+ // by the checkbox.
+ annotationApi.addNotifier(notifyFunc => {
+ new Promise(resolve => setTimeout(resolve, 3000)).then(() => {
+ populateWithDummyData(coverageData);
+ checkbox.disabled = false;
+ checkbox.onclick = e => {
+ displayCoverage = e.target.checked;
+ Object.keys(coverageData).forEach(file => {
+ notifyFunc(file, 0, coverageData[file].totalLines, 'right');
+ });
+ };
+ });
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index c109381..7ba4fc6 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -171,7 +171,7 @@
}
.owner,
.size {
- width: auto;
+ max-width: none;
}
}
@media only screen and (min-width: 1450px) {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c3d856e..2bb8b2e 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -127,6 +127,7 @@
'settings/gr-cla-view/gr-cla-view_test.html',
'settings/gr-edit-preferences/gr-edit-preferences_test.html',
'settings/gr-email-editor/gr-email-editor_test.html',
+ 'settings/gr-gpg-editor/gr-gpg-editor_test.html',
'settings/gr-group-list/gr-group-list_test.html',
'settings/gr-http-password/gr-http-password_test.html',
'settings/gr-identities/gr-identities_test.html',
diff --git a/resources/com/google/gerrit/server/mail/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index 1d6ae9c..40924e6 100644
--- a/resources/com/google/gerrit/server/mail/Merged.soy
+++ b/resources/com/google/gerrit/server/mail/Merged.soy
@@ -1,3 +1,4 @@
+
/**
* Copyright (C) 2016 The Android Open Source Project
*
@@ -21,16 +22,10 @@
* a change successfully merged to the head.
* @param change
* @param email
- * @param fromEmail
* @param fromName
- * @param patchSetInfo
*/
{template .Merged kind="text"}
- {$fromName} merged this change
- {if $patchSetInfo.authorEmail != $fromEmail}
- {sp}by {$patchSetInfo.authorName}
- {/if}.
-
+ {$fromName} has submitted this change and it was merged.
{if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
{\n}
Change subject: {$change.subject}{\n}
diff --git a/resources/com/google/gerrit/server/mail/MergedHtml.soy b/resources/com/google/gerrit/server/mail/MergedHtml.soy
index 414479e..b11c5e5 100644
--- a/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -19,16 +19,11 @@
/**
* @param diffLines
* @param email
- * @param fromEmail
* @param fromName
- * @param patchSetInfo
*/
{template .MergedHtml}
<p>
- {$fromName} <strong>merged</strong> this change
- {if $patchSetInfo.authorEmail != $fromEmail}
- {sp}by {$patchSetInfo.authorName}
- {/if}.
+ {$fromName} <strong>merged</strong> this change.
</p>
{if $email.changeUrl}
diff --git a/tools/release-announcement.py b/tools/release-announcement.py
index 83a78fe..f700185 100755
--- a/tools/release-announcement.py
+++ b/tools/release-announcement.py
@@ -142,7 +142,10 @@
if not os.path.isdir(gpghome):
print("Skipping signing due to missing gnupg home folder")
else:
- gpg = GPG(homedir=gpghome)
+ try:
+ gpg = GPG(homedir=gpghome)
+ except TypeError:
+ gpg = GPG(gnupghome=gpghome)
signed = gpg.sign(output)
filename = filename + ".asc"
with open(filename, "w") as f: