Merge "Make sure threads have commentSide attribute"
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/WORKSPACE b/WORKSPACE
index 7852958..ac0ffd2 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1099,6 +1099,13 @@
)
bower_archive(
+ name = "paper-tabs",
+ package = "polymerelements/paper-tabs",
+ sha1 = "b6dd2fbd7ee887534334057a29eb545b940fc5cf",
+ version = "2.0.0",
+)
+
+bower_archive(
name = "iron-icon",
package = "polymerelements/iron-icon",
sha1 = "7da49a0d33cd56017740e0dbcf41d2b71532023f",
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index a68266a..15a330e 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -84,7 +84,6 @@
dbInjector = createDbInjector(MULTI_USER);
globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
threads = ThreadLimiter.limitThreads(dbInjector, threads);
- checkNotSlaveMode();
overrideConfig();
LifecycleManager dbManager = new LifecycleManager();
dbManager.add(dbInjector);
@@ -141,26 +140,21 @@
"invalid index name(s): " + new TreeSet<>(invalid) + " available indices are: " + valid);
}
- private void checkNotSlaveMode() throws Die {
- if (globalConfig.getBoolean("container", "slave", false)) {
- throw die("Cannot run reindex in slave mode");
- }
- }
-
private Injector createSysInjector() {
Map<String, Integer> versions = new HashMap<>();
if (changesVersion != null) {
versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
}
+ boolean slave = globalConfig.getBoolean("container", "slave", false);
List<Module> modules = new ArrayList<>();
Module indexModule;
switch (IndexModule.getIndexType(dbInjector)) {
case LUCENE:
- indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, false);
+ indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
break;
case ELASTICSEARCH:
indexModule =
- ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, false);
+ ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
break;
default:
throw new IllegalStateException("unsupported index.type");
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 1485ee3..5143dc7 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -22,6 +22,7 @@
import com.google.gerrit.server.mail.MailHeader;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import org.apache.james.mime4j.dom.field.FieldName;
/** Send an email to inform users that parsing their inbound email failed. */
public class InboundEmailRejectionSender extends OutgoingEmail {
@@ -55,11 +56,12 @@
protected void init() throws EmailException {
super.init();
setListIdHeader();
+ setHeader(FieldName.SUBJECT, "[Gerrit Code Review] Unable to process your email");
add(RecipientType.TO, to);
if (!threadId.isEmpty()) {
- setHeader(MailHeader.REFERENCES.fieldName(), "<" + threadId + ">");
+ setHeader(MailHeader.REFERENCES.fieldName(), threadId);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
index f2a5d2f..4b6f8b2 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -16,6 +16,7 @@
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
@@ -27,6 +28,7 @@
import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.index.GerritIndexStatus;
@@ -36,6 +38,8 @@
import com.google.inject.Provider;
import java.nio.file.Files;
import java.util.Set;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.junit.Test;
@@ -80,6 +84,91 @@
}
@Test
+ public void offlineReindexForChangesIsNotPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "changes",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts shouldn't allow to offline reindex changes")
+ .that(exitCode)
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void offlineReindexForAccountsIsNotPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "accounts",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts shouldn't allow to offline reindex accounts")
+ .that(exitCode)
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void offlineReindexForProjectsIsNotPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "projects",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts shouldn't allow to offline reindex projects")
+ .that(exitCode)
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void offlineReindexForGroupsIsPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "groups",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts should allow to offline reindex groups")
+ .that(exitCode)
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void offlineReindexForAllAvailableIndicesIsPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+
+ assertWithMessage("Slave hosts should allow to perform a general offline reindex")
+ .that(exitCode)
+ .isEqualTo(0);
+ }
+
+ @Test
public void onlineUpgradeChanges() throws Exception {
int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
@@ -143,12 +232,24 @@
}
private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+ updateConfig(cfg -> cfg.setBoolean("index", null, "onlineUpgrade", enable));
+ }
+
+ private void enableSlaveMode() throws Exception {
+ updateConfig(config -> config.setBoolean("container", null, "slave", true));
+ }
+
+ private void updateConfig(Consumer<Config> configConsumer) throws Exception {
FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
cfg.load();
- cfg.setBoolean("index", null, "onlineUpgrade", enable);
+ configConsumer.accept(cfg);
cfg.save();
}
+ private static int runGerritAndReturnExitCode(String... args) throws Exception {
+ return GerritLauncher.mainImpl(args);
+ }
+
private void assertSearchVersion(ServerContext ctx, int expected) {
assertThat(
ctx.getInjector()
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/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 71976f5..f34fe33 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -255,5 +255,6 @@
assertNotifyTo(user);
Message message = sender.nextMessage();
assertThat(message.body()).contains("was unable to parse your email");
+ assertThat(message.headers()).containsKey("Subject");
}
}
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index c035793..5ee3535 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -25,8 +25,8 @@
bower_archive(
name = "font-roboto",
package = "PolymerElements/font-roboto",
- version = "1.0.3",
- sha1 = "edf478d20ae2fc0704d7c155e20162caaabdd5ae")
+ version = "1.1.0",
+ sha1 = "ab4218d87b9ce569d6282b01f7642e551879c3d5")
bower_archive(
name = "iron-a11y-announcer",
package = "PolymerElements/iron-a11y-announcer",
@@ -39,7 +39,7 @@
sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465")
bower_archive(
name = "iron-behaviors",
- package = "polymerelements/iron-behaviors",
+ package = "PolymerElements/iron-behaviors",
version = "1.0.18",
sha1 = "e231a1a02b090f5183db917639fdb96cdd0dca18")
bower_archive(
@@ -55,8 +55,8 @@
bower_archive(
name = "iron-flex-layout",
package = "PolymerElements/iron-flex-layout",
- version = "1.3.7",
- sha1 = "4d4cf3232cf750a17a7df0a37476117f831ac633")
+ version = "1.3.9",
+ sha1 = "d987b924cf29fcfe4b393833e81fdc9f1e268796")
bower_archive(
name = "iron-form-element-behavior",
package = "PolymerElements/iron-form-element-behavior",
@@ -103,6 +103,11 @@
version = "1.0.13",
sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7")
bower_archive(
+ name = "paper-icon-button",
+ package = "PolymerElements/paper-icon-button",
+ version = "2.1.0",
+ sha1 = "caead6a276877888d128ace809376980c3f3fe42")
+ bower_archive(
name = "paper-ripple",
package = "PolymerElements/paper-ripple",
version = "1.0.10",
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index fb40855..dc16ccf 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -229,6 +229,16 @@
seed = True,
)
bower_component(
+ name = "paper-icon-button",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-icon",
+ ":paper-behaviors",
+ ":paper-styles",
+ ":polymer",
+ ],
+ )
+ bower_component(
name = "paper-input",
license = "//lib:LICENSE-polymer",
deps = [
@@ -282,6 +292,23 @@
],
)
bower_component(
+ name = "paper-tabs",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-behaviors",
+ ":iron-flex-layout",
+ ":iron-icon",
+ ":iron-iconset-svg",
+ ":iron-menu-behavior",
+ ":iron-resizable-behavior",
+ ":paper-behaviors",
+ ":paper-icon-button",
+ ":paper-styles",
+ ":polymer",
+ ],
+ seed = True,
+ )
+ bower_component(
name = "paper-toggle-button",
license = "//lib:LICENSE-polymer",
deps = [
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 0c2cd5e..7487ad5 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -26,6 +26,7 @@
"//lib/js:paper-input",
"//lib/js:paper-item",
"//lib/js:paper-listbox",
+ "//lib/js:paper-tabs",
"//lib/js:paper-toggle-button",
"//lib/js:polymer",
"//lib/js:polymer-resin",
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 1ce05b1..5257e69 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -15,6 +15,8 @@
'use strict';
const SUGGESTIONS_LIMIT = 15;
+ const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
+ 'permission to add it';
const URL_REGEX = '^(?:[a-z]+:)?//';
@@ -186,7 +188,16 @@
_handleSavingIncludedGroups() {
return this.$.restAPI.saveIncludedGroup(this._groupName,
- this._includedGroupSearch)
+ this._includedGroupSearch, err => {
+ if (err.status === 404) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: SAVING_ERROR_TEXT},
+ bubbles: true,
+ }));
+ return err;
+ }
+ throw Error(err.statusText);
+ })
.then(config => {
if (!config) {
return;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 194750e..d670d4d 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -183,6 +183,24 @@
});
});
+ test('add included group 404 shows helpful error text', () => {
+ element._groupOwner = true;
+
+ const memberName = 'bad-name';
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+ () => Promise.reject({status: 404}));
+
+ element.$.groupMemberSearchInput.text = memberName;
+ element.$.groupMemberSearchInput.value = 1234;
+
+ return element._handleSavingIncludedGroups().then(() => {
+ assert.isTrue(alertStub.called);
+ });
+ });
+
test('_getAccountSuggestions empty', () => {
return element._getAccountSuggestions('nonexistent').then(accounts => {
assert.equal(accounts.length, 0);
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 e5e077f..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);
}
@@ -107,6 +123,9 @@
:host {
display: flex;
}
+ :host([selected]) {
+ border-left: none;
+ }
}
</style>
<style include="gr-change-list-styles"></style>
@@ -133,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>
@@ -180,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-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index f300992..cb2eebe 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -15,20 +15,16 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-storage/gr-storage.html">
<link rel="import" href="../gr-diff-comment/gr-diff-comment.html">
-<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-diff-comment-thread">
<template>
<style include="shared-styles">
- :host {
- border: 1px solid #bbb;
- display: block;
- margin-bottom: 1px;
- white-space: normal;
- }
gr-button {
margin-left: .5em;
--gr-button-color: #212121;
@@ -39,6 +35,10 @@
}
#container {
background-color: #fcfad6;
+ border: 1px solid #bbb;
+ display: block;
+ margin-bottom: 1px;
+ white-space: normal;
}
#container.unresolved {
background-color: #fcfaa6;
@@ -52,7 +52,22 @@
margin: auto 0;
padding: .5em .7em;
}
+ .pathInfo {
+ display: flex;
+ align-items: baseline;
+ }
+ .descriptionText {
+ margin-left: .5rem;
+ font-size: var(--font-size-small);
+ font-style: italic;
+ }
</style>
+ <template is="dom-if" if="[[showFilePath]]">
+ <div class="pathInfo">
+ <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
+ <span class="descriptionText">Patchset [[patchNum]]</span>
+ </div>
+ </template>
<div id="container" class$="[[_computeHostClass(unresolved)]]">
<template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
as="comment">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 4de073e..e14370c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -58,6 +58,16 @@
value: null,
},
rootId: String,
+ /**
+ * If this is true, the comment thread also needs to have the change and
+ * line properties property set
+ */
+ showFilePath: {
+ type: Boolean,
+ value: false,
+ },
+ /** Necessary only if showFilePath is true */
+ lineNum: Number,
unresolved: {
type: Boolean,
notify: true,
@@ -71,6 +81,7 @@
behaviors: [
Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PathListBehavior,
],
listeners: {
@@ -117,6 +128,17 @@
this.push('comments', draft);
},
+ _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
+ return Gerrit.Nav.getUrlForDiffById(changeNum,
+ projectName, path, patchNum,
+ null, this.lineNum);
+ },
+
+ _computeDisplayPath(path) {
+ const lineString = this.lineNum ? `#${this.lineNum}` : '';
+ return this.computeDisplayPath(path) + lineString;
+ },
+
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index c84c260..de6c3b01 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -170,6 +170,35 @@
done();
});
});
+
+ test('optionally show file path', () => {
+ // Path info doesn't exist when showFilePath is false. Because it's in a
+ // dom-if it is not yet in the dom.
+ assert.isNotOk(element.$$('.pathInfo'));
+
+ sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ element.changeNum = 123;
+ element.projectName = 'test project';
+ element.path = 'path/to/file';
+ element.patchNum = 3;
+ element.lineNum = 5;
+ element.showFilePath = true;
+ flushAsynchronousOperations();
+ assert.isOk(element.$$('.pathInfo'));
+ assert.notEqual(getComputedStyle(element.$$('.pathInfo')).display,
+ 'none');
+ assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
+ element.changeNum, element.projectName, element.path,
+ element.patchNum, null, element.lineNum));
+ });
+
+ test('_computeDisplayPath', () => {
+ const path = 'path/to/file';
+ assert.equal(element._computeDisplayPath(path), 'path/to/file');
+
+ element.lineNum = 5;
+ assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
+ });
});
suite('comment action tests', () => {
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 6749437..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
@@ -437,12 +437,16 @@
.then(response => this.getResponseObject(response));
},
- saveIncludedGroup(groupName, includedGroup) {
+ saveIncludedGroup(groupName, includedGroup, opt_errFn) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
return this.send('PUT',
- `/groups/${encodeName}/groups/${encodeIncludedGroup}`)
- .then(response => this.getResponseObject(response));
+ `/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
+ opt_errFn).then(response => {
+ if (response.ok) {
+ return this.getResponseObject(response);
+ }
+ });
},
deleteGroupMembers(groupName, groupMembers) {
@@ -1948,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/js/bower2bazel.py b/tools/js/bower2bazel.py
index 7479021..ccdf2df 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -58,11 +58,13 @@
"neon-animation": "polymer",
"page": "page.js",
"paper-button": "polymer",
+ "paper-icon-button": "polymer",
"paper-input": "polymer",
"paper-item": "polymer",
"paper-listbox": "polymer",
"paper-toggle-button": "polymer",
"paper-styles": "polymer",
+ "paper-tabs": "polymer",
"polymer": "polymer",
"polymer-resin": "polymer",
"promise-polyfill": "promise-polyfill",
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: