Merge changes from topic "get-rid-of-joda-time" * changes: Guard against null values for date during mail parsing Provide Joda time only for Elasticsearch Don't export Joda time for plugins anymore Remove joda-time from BUILD files for core Gerrit Switch to Java Date/Time API for TimeUtil Switch to Java Date/Time API for mail related code Switch to Java Date/Time API in ScheduleConfig Switch to Java Date/Time API in ResourceServletTest Switch to Java Date/Time API in ChangeBundleTest OutputStreamQueryTest: Remove test proving equivalence to Joda time Switch to Java Date/Time API in OutputStreamQuery
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index 6e1f394..93910d9 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -628,6 +628,23 @@ + By default, true. +[[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus:: ++ +Whether to allow automatic synchronization of an account's inactive flag upon login. +If set to true, upon login, if the authentication back-end reports the account as active, +the account's inactive flag in the internal Gerrit database will be updated to be active. +If the authentication back-end reports the account as inactive, the account's flag will be +updated to be inactive and the login attempt will be blocked. Users enabling this feature +should ensure that their authentication back-end is supported. Currently, only +strict 'LDAP' authentication is supported. ++ +In addition, if this parameter is not set, or false, the corresponding scheduled +task to deactivate inactive Gerrit accounts will also be disabled. If this +parameter is set to true, users should also consider configuring the +link:#accountDeactivation[accountDeactivation] section appropriately. ++ +By default, false. + [[cache]] === Section cache @@ -4538,6 +4555,44 @@ If no groups are added, any user will be allowed to execute 'upload-pack' on the server. +[[accountDeactivation]] +=== Section accountDeactivation + +Configures the parameters for the scheduled task to sweep and deactivate Gerrit +accounts according to their status reported by the auth backend. Currently only +supported for LDAP backends. + +[[accountDeactivation.startTime]]accountDeactivation.startTime:: ++ +Start time to define the first execution of account deactivations. +If the configured `'accountDeactivation.interval'` is shorter than `'accountDeactivation.startTime - now'` +the start time will be preponed by the maximum integral multiple of +`'accountDeactivation.interval'` so that the start time is still in the future. ++ +---- +<day of week> <hours>:<minutes> +or +<hours>:<minutes> + +<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun +<hours> : 00-23 +<minutes> : 0-59 +---- + +[[accountDeactivation.interval]]accountDeactivation.interval:: ++ +Interval for periodic repetition of triggering account deactivation sweeps. +The interval must be larger than zero. The following suffixes are supported +to define the time unit for the interval: ++ +* `s, sec, second, seconds` +* `m, min, minute, minutes` +* `h, hr, hour, hours` +* `d, day, days` +* `w, week, weeks` (`1 week` is treated as `7 days`) +* `mon, month, months` (`1 month` is treated as `30 days`) +* `y, year, years` (`1 year` is treated as `365 days`) + [[urlAlias]] === Section urlAlias
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt index 2f6a7d6..039d545 100644 --- a/Documentation/dev-release.txt +++ b/Documentation/dev-release.txt
@@ -89,14 +89,22 @@ . link:#merge-stable[Merge `stable` into `master`] -[[update-version]] -=== Update Version and Create Release Tag +[[update-versions]] +=== Update Versions and Create Release Tag Before doing the release build, the `GERRIT_VERSION` in the `version.bzl` file must be updated, e.g. change it from `2.5-SNAPSHOT` to `2.5`. -Commit the change in `version.bzl` and create a signed release tag on the -new commit: +In addition the version must be updated in a number of pom.xml files. + +To do this run the `./tools/version.py` script and provide the new +version as parameter, e.g.: + +---- + ./tools/version.py 2.5 +---- + +Commit the changes and create a signed release tag on the new commit: ---- git tag -s -m "v2.5" v2.5 @@ -139,8 +147,9 @@ link:dev-release-deploy-config.html#deploy-configuration-setting-maven-central[ configuration] for deploying to Maven Central -* Make sure that the version is updated in the `version.bzl` file as described -in the link:#update-version[Update Version and Create Release Tag] section. +* Make sure that the version is updated in the `version.bzl` file and in +the `pom.xml` files as described in the link:#update-versions[Update +Versions and Create Release Tag] section. * Push the WAR to Maven Central: +
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt index 1fb871a..a83ad44 100644 --- a/Documentation/dev-stars.txt +++ b/Documentation/dev-stars.txt
@@ -61,18 +61,24 @@ The ignore star is represented by the special star label 'ignore'. -[[mute-star]] -== Mute Star +[[reviewed-star]] +== Reviewed Star -If the "mute/<patchset_id>"-star is set by a user, and <patchset_id> +If the "reviewed/<patchset_id>"-star is set by a user, and <patchset_id> matches the current patch set, the change is always reported as "reviewed" in the ChangeInfo. This allows users to "de-highlight" changes in a dashboard until a new patchset has been uploaded. -The ChangeInfo muted-field will show if the change is currently in a -mute state. +[[unreviewed-star]] +== Unreviewed Star + +If the "unreviewed/<patchset_id>"-star is set by a user, and <patchset_id> +matches the current patch set, the change is always reported as "unreviewed" +in the ChangeInfo. + +This allows users to "highlight" changes in a dashboard. [[query-stars]] == Query Stars
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt index da81dff..6b9374f 100644 --- a/Documentation/linux-quickstart.txt +++ b/Documentation/linux-quickstart.txt
@@ -73,7 +73,7 @@ == Update the listen URL Another recommended task is to change the URL that Gerrit listens to from `*` -to `localhost`. This changes helps prevent outside connections from contacting +to `localhost`. This change helps prevent outside connections from contacting the instance. ....
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt index bc0a3cf..d902a1ce 100644 --- a/Documentation/note-db.txt +++ b/Documentation/note-db.txt
@@ -95,7 +95,7 @@ To run the offline migration, run the `migrate-to-note-db` program: ---- - java -jar gerrit.war migrate-to-note-db /path/to/site + java -jar gerrit.war migrate-to-note-db -d /path/to/site ---- Once started, it is safe to cancel and restart the migration process, or to @@ -124,7 +124,7 @@ To run the migration in trial mode, add `--trial` to `migrate-to-note-db` or `daemon`: ---- - java -jar gerrit.war migrate-to-note-db --trial /path/to/site + java -jar gerrit.war migrate-to-note-db --trial -d /path/to/site # OR java -jar gerrit.war daemon -d /path/to/site --migrate-to-note-db --trial ----
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index fa91c2d..50afe40 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -2329,13 +2329,13 @@ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0 ---- -[[mute]] -=== Mute +[[mark-as-reviewed]] +=== Mark as Reviewed -- -'PUT /changes/link:#change-id[\{change-id\}]/mute' +'PUT /changes/link:#change-id[\{change-id\}]/reviewed' -- -Marks a change as muted. +Marks a change as reviewed. This allows users to "de-highlight" changes in their dashboard until a new patch set is uploaded. @@ -2347,20 +2347,22 @@ .Request ---- - PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/mute HTTP/1.0 + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewed HTTP/1.0 ---- -[[unmute]] -=== Unmute +[[mark-as-unreviewed]] +=== Mark as Unreviewed -- -'PUT /changes/link:#change-id[\{change-id\}]/unmute' +'PUT /changes/link:#change-id[\{change-id\}]/unreviewed' -- -Unmutes a change. +Marks a change as unreviewed. + +This allows users to "highlight" changes in their dashboard .Request ---- - PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0 + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unreviewed HTTP/1.0 ---- [[get-hashtags]] @@ -5641,8 +5643,6 @@ change. The labels are lexicographically sorted. |`reviewed` |not set if `false`| Whether the change was reviewed by the calling user. -|`muted` |not set if `false`| -Whether the change has been link:#mute[muted] by the calling user. Only set if link:#reviewed[reviewed] is requested. |`submit_type` |optional| The link:project-configuration.html#submit_type[submit type] of the change. +
diff --git a/WORKSPACE b/WORKSPACE index 8c92ad1..6458571 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -910,8 +910,8 @@ # When upgrading Elasticsearch, make sure it's compatible with Lucene maven_jar( name = "elasticsearch", - artifact = "org.elasticsearch:elasticsearch:2.4.5", - sha1 = "daafe48ae06592029a2fedca1fe2ac0f5eec3185", + artifact = "org.elasticsearch:elasticsearch:2.4.6", + sha1 = "d2954e1173a608a9711f132d1768a676a8b1fb81", ) # Java REST client for Elasticsearch.
diff --git a/fake_pom.xml b/gerrit-acceptance-framework/pom.xml similarity index 85% copy from fake_pom.xml copy to gerrit-acceptance-framework/pom.xml index 6ec45e5..a0f2e67 100644 --- a/fake_pom.xml +++ b/gerrit-acceptance-framework/pom.xml
@@ -1,11 +1,11 @@ <project> <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> - <artifactId>gerrit</artifactId> - <version>1</version> <!-- Do not edit; see version.bzl. --> + <artifactId>gerrit-acceptance-framework</artifactId> + <version>2.16-SNAPSHOT</version> <packaging>jar</packaging> - <name>Gerrit Code Review - Extension API</name> - <description>API for Gerrit Extensions</description> + <name>Gerrit Code Review - Acceptance Test Framework</name> + <description>Framework for Gerrit's acceptance tests</description> <url>https://www.gerritcodereview.com/</url> <licenses> @@ -53,15 +53,9 @@ <name>Logan Hanks</name> </developer> <developer> - <name>Luca Milanesio</name> - </developer> - <developer> <name>Martin Fick</name> </developer> <developer> - <name>Patrick Hiesel</name> - </developer> - <developer> <name>Saša Živkov</name> </developer> <developer>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java index 3174090..a508948 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -19,6 +19,7 @@ import com.google.gerrit.acceptance.InProcessProtocol.Context; import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -34,8 +35,13 @@ import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.receive.AsyncReceiveCommits; import com.google.gerrit.server.git.validators.UploadValidators; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.RequestScopePropagator; import com.google.gerrit.server.util.ThreadLocalRequestContext; @@ -203,29 +209,32 @@ private static class Upload implements UploadPackFactory<Context> { private final Provider<CurrentUser> userProvider; - private final ProjectControl.GenericFactory projectControlFactory; private final VisibleRefFilter.Factory refFilterFactory; private final TransferConfig transferConfig; private final DynamicSet<PreUploadHook> preUploadHooks; private final UploadValidators.Factory uploadValidatorsFactory; private final ThreadLocalRequestContext threadContext; + private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; @Inject Upload( Provider<CurrentUser> userProvider, - ProjectControl.GenericFactory projectControlFactory, VisibleRefFilter.Factory refFilterFactory, TransferConfig transferConfig, DynamicSet<PreUploadHook> preUploadHooks, UploadValidators.Factory uploadValidatorsFactory, - ThreadLocalRequestContext threadContext) { + ThreadLocalRequestContext threadContext, + ProjectCache projectCache, + PermissionBackend permissionBackend) { this.userProvider = userProvider; - this.projectControlFactory = projectControlFactory; this.refFilterFactory = refFilterFactory; this.transferConfig = transferConfig; this.preUploadHooks = preUploadHooks; this.uploadValidatorsFactory = uploadValidatorsFactory; this.threadContext = threadContext; + this.projectCache = projectCache; + this.permissionBackend = permissionBackend; } @Override @@ -236,23 +245,35 @@ // its original context anyway. threadContext.setContext(req); current.set(req); - try { - ProjectControl ctl = projectControlFactory.controlFor(req.project, userProvider.get()); - if (!ctl.canRunUploadPack()) { - throw new ServiceNotAuthorizedException(); - } - UploadPack up = new UploadPack(repo); - up.setPackConfig(transferConfig.getPackConfig()); - up.setTimeout(transferConfig.getTimeout()); - up.setAdvertiseRefsHook(refFilterFactory.create(ctl.getProjectState(), repo)); - List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks); - hooks.add(uploadValidatorsFactory.create(ctl.getProject(), repo, "localhost-test")); - up.setPreUploadHook(PreUploadHookChain.newChain(hooks)); - return up; - } catch (NoSuchProjectException | IOException e) { + try { + permissionBackend + .user(userProvider) + .project(req.project) + .check(ProjectPermission.RUN_UPLOAD_PACK); + } catch (AuthException e) { + throw new ServiceNotAuthorizedException(); + } catch (PermissionBackendException e) { throw new RuntimeException(e); } + + ProjectState projectState; + try { + projectState = projectCache.checkedGet(req.project); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (projectState == null) { + throw new RuntimeException("can't load project state for " + req.project.get()); + } + UploadPack up = new UploadPack(repo); + up.setPackConfig(transferConfig.getPackConfig()); + up.setTimeout(transferConfig.getTimeout()); + up.setAdvertiseRefsHook(refFilterFactory.create(projectState, repo)); + List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks); + hooks.add(uploadValidatorsFactory.create(projectState.getProject(), repo, "localhost-test")); + up.setPreUploadHook(PreUploadHookChain.newChain(hooks)); + return up; } } @@ -264,6 +285,7 @@ private final DynamicSet<ReceivePackInitializer> receivePackInitializers; private final DynamicSet<PostReceiveHook> postReceiveHooks; private final ThreadLocalRequestContext threadContext; + private final PermissionBackend permissionBackend; @Inject Receive( @@ -273,7 +295,8 @@ TransferConfig config, DynamicSet<ReceivePackInitializer> receivePackInitializers, DynamicSet<PostReceiveHook> postReceiveHooks, - ThreadLocalRequestContext threadContext) { + ThreadLocalRequestContext threadContext, + PermissionBackend permissionBackend) { this.userProvider = userProvider; this.projectControlFactory = projectControlFactory; this.factory = factory; @@ -281,6 +304,7 @@ this.receivePackInitializers = receivePackInitializers; this.postReceiveHooks = postReceiveHooks; this.threadContext = threadContext; + this.permissionBackend = permissionBackend; } @Override @@ -292,11 +316,17 @@ threadContext.setContext(req); current.set(req); try { + permissionBackend + .user(userProvider) + .project(req.project) + .check(ProjectPermission.RUN_RECEIVE_PACK); + } catch (AuthException e) { + throw new ServiceNotAuthorizedException(); + } catch (PermissionBackendException e) { + throw new RuntimeException(e); + } + try { ProjectControl ctl = projectControlFactory.controlFor(req.project, userProvider.get()); - if (!ctl.canRunReceivePack()) { - throw new ServiceNotAuthorizedException(); - } - AsyncReceiveCommits arc = factory.create(ctl, db, null, ImmutableSetMultimap.of()); ReceivePack rp = arc.getReceivePack();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java index 65e5909..c2d3184 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3374,7 +3374,7 @@ } @Test - public void mute() throws Exception { + public void markAsReviewed() throws Exception { TestAccount user2 = accountCreator.user2(); PushOneCommit.Result r = createChange(); @@ -3384,16 +3384,16 @@ gApi.changes().id(r.getChangeId()).addReviewer(in); setApiUser(user); - assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse(); - gApi.changes().id(r.getChangeId()).mute(true); - assertThat(gApi.changes().id(r.getChangeId()).muted()).isTrue(); + assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull(); + gApi.changes().id(r.getChangeId()).markAsReviewed(true); + assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue(); setApiUser(user2); sender.clear(); amendChange(r.getChangeId()); setApiUser(user); - assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse(); + assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull(); List<Message> messages = sender.getMessages(); assertThat(messages).hasSize(1); @@ -3401,12 +3401,73 @@ } @Test - public void cannotMuteOwnChange() throws Exception { + public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception { String changeId = createChange().getChangeId(); + setApiUser(user); + gApi.changes().id(changeId).markAsReviewed(true); + assertThat(gApi.changes().id(changeId).get().reviewed).isTrue(); + exception.expect(BadRequestException.class); - exception.expectMessage("cannot mute own change"); - gApi.changes().id(changeId).mute(true); + exception.expectMessage( + "The labels " + + StarredChangesUtil.REVIEWED_LABEL + + "/" + + 1 + + " and " + + StarredChangesUtil.UNREVIEWED_LABEL + + "/" + + 1 + + " are mutually exclusive. Only one of them can be set."); + gApi.accounts() + .self() + .setStars( + changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1"))); + } + + @Test + public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception { + String changeId = createChange().getChangeId(); + + setApiUser(user); + gApi.changes().id(changeId).markAsReviewed(false); + assertThat(gApi.changes().id(changeId).get().reviewed).isNull(); + + exception.expect(BadRequestException.class); + exception.expectMessage( + "The labels " + + StarredChangesUtil.REVIEWED_LABEL + + "/" + + 1 + + " and " + + StarredChangesUtil.UNREVIEWED_LABEL + + "/" + + 1 + + " are mutually exclusive. Only one of them can be set."); + gApi.accounts() + .self() + .setStars( + changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1"))); + } + + @Test + public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception { + String changeId = createChange().getChangeId(); + + setApiUser(user); + gApi.changes().id(changeId).markAsReviewed(true); + assertThat(gApi.changes().id(changeId).get().reviewed).isTrue(); + + amendChange(changeId); + assertThat(gApi.changes().id(changeId).get().reviewed).isNull(); + + gApi.changes().id(changeId).markAsReviewed(false); + assertThat(gApi.changes().id(changeId).get().reviewed).isNull(); + + assertThat(gApi.accounts().self().getStars(changeId)) + .containsExactly( + StarredChangesUtil.REVIEWED_LABEL + "/" + 1, + StarredChangesUtil.UNREVIEWED_LABEL + "/" + 2); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java index bfe6b8b..7d5072a 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -14,22 +14,159 @@ package com.google.gerrit.acceptance.api.project; +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.util.stream.Collectors.toList; + import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.api.projects.DashboardInfo; +import com.google.gerrit.extensions.api.projects.ProjectApi; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.project.DashboardsCollection; +import java.util.List; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; import org.junit.Test; @NoHttpd public class DashboardIT extends AbstractDaemonTest { + @Before + public void setup() throws Exception { + allow("refs/meta/dashboards/*", Permission.CREATE, REGISTERED_USERS); + } + @Test public void defaultDashboardDoesNotExist() throws Exception { exception.expect(ResourceNotFoundException.class); - gApi.projects().name(project.get()).defaultDashboard().get(); + project().defaultDashboard().get(); } @Test public void dashboardDoesNotExist() throws Exception { exception.expect(ResourceNotFoundException.class); - gApi.projects().name(project.get()).dashboard("my:dashboard").get(); + project().dashboard("my:dashboard").get(); + } + + @Test + public void getDashboard() throws Exception { + DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test"); + DashboardInfo result = project().dashboard(info.id).get(); + assertDashboardInfo(result, info); + } + + @Test + public void getDashboardNonDefault() throws Exception { + DashboardInfo info = createDashboard("my", "test"); + DashboardInfo result = project().dashboard(info.id).get(); + assertDashboardInfo(result, info); + } + + @Test + public void listDashboards() throws Exception { + assertThat(dashboards()).isEmpty(); + DashboardInfo info1 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test1"); + DashboardInfo info2 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test2"); + assertThat(dashboards().stream().map(d -> d.id).collect(toList())) + .containsExactly(info1.id, info2.id); + } + + @Test + public void setDefaultDashboard() throws Exception { + DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test"); + assertThat(info.isDefault).isNull(); + project().dashboard(info.id).setDefault(); + assertThat(project().dashboard(info.id).get().isDefault).isTrue(); + assertThat(project().defaultDashboard().get().id).isEqualTo(info.id); + } + + @Test + public void setDefaultDashboardByProject() throws Exception { + DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test"); + assertThat(info.isDefault).isNull(); + project().defaultDashboard(info.id); + assertThat(project().dashboard(info.id).get().isDefault).isTrue(); + assertThat(project().defaultDashboard().get().id).isEqualTo(info.id); + + project().removeDefaultDashboard(); + assertThat(project().dashboard(info.id).get().isDefault).isNull(); + + exception.expect(ResourceNotFoundException.class); + project().defaultDashboard().get(); + } + + @Test + public void replaceDefaultDashboard() throws Exception { + DashboardInfo d1 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test1"); + DashboardInfo d2 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test2"); + assertThat(d1.isDefault).isNull(); + assertThat(d2.isDefault).isNull(); + project().dashboard(d1.id).setDefault(); + assertThat(project().dashboard(d1.id).get().isDefault).isTrue(); + assertThat(project().dashboard(d2.id).get().isDefault).isNull(); + assertThat(project().defaultDashboard().get().id).isEqualTo(d1.id); + project().dashboard(d2.id).setDefault(); + assertThat(project().defaultDashboard().get().id).isEqualTo(d2.id); + assertThat(project().dashboard(d1.id).get().isDefault).isNull(); + assertThat(project().dashboard(d2.id).get().isDefault).isTrue(); + } + + @Test + public void cannotGetDashboardWithInheritedForNonDefault() throws Exception { + DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test"); + exception.expect(BadRequestException.class); + exception.expectMessage("inherited flag can only be used with default"); + project().dashboard(info.id).get(true); + } + + private void assertDashboardInfo(DashboardInfo actual, DashboardInfo expected) throws Exception { + assertThat(actual.id).isEqualTo(expected.id); + assertThat(actual.path).isEqualTo(expected.path); + assertThat(actual.ref).isEqualTo(expected.ref); + assertThat(actual.project).isEqualTo(project.get()); + assertThat(actual.definingProject).isEqualTo(project.get()); + } + + private List<DashboardInfo> dashboards() throws Exception { + return project().dashboards().get(); + } + + private ProjectApi project() throws RestApiException { + return gApi.projects().name(project.get()); + } + + private DashboardInfo createDashboard(String ref, String path) throws Exception { + DashboardInfo info = DashboardsCollection.newDashboardInfo(ref, path); + String canonicalRef = DashboardsCollection.normalizeDashboardRef(info.ref); + try { + project().branch(canonicalRef).create(new BranchInput()); + } catch (ResourceConflictException e) { + // The branch already exists if this method has already been called once. + if (!e.getMessage().contains("already exists")) { + throw e; + } + } + try (Repository r = repoManager.openRepository(project)) { + TestRepository<Repository>.CommitBuilder cb = + new TestRepository<>(r).branch(canonicalRef).commit(); + String content = + "[dashboard]\n" + + "Title = Reviewer\n" + + "Description = Own review requests\n" + + "foreach = owner:self\n" + + "[section \"Open\"]\n" + + "query = is:open"; + cb.add(info.path, content); + RevCommit c = cb.create(); + project().commit(c.name()); + } + return info; } }
diff --git a/fake_pom.xml b/gerrit-extension-api/pom.xml similarity index 95% rename from fake_pom.xml rename to gerrit-extension-api/pom.xml index 6ec45e5..a8ae2e6 100644 --- a/fake_pom.xml +++ b/gerrit-extension-api/pom.xml
@@ -1,8 +1,8 @@ <project> <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> - <artifactId>gerrit</artifactId> - <version>1</version> <!-- Do not edit; see version.bzl. --> + <artifactId>gerrit-extension-api</artifactId> + <version>2.16-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Extension API</name> <description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java index 1fc04b4..481681e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -119,18 +119,12 @@ boolean ignored() throws RestApiException; /** - * Mute or un-mute this change. + * Mark this change as reviewed/unreviewed. * - * @param mute mute the change if true + * @param reviewed flag to decide if this change should be marked as reviewed ({@code true}) or + * unreviewed ({@code false}) */ - void mute(boolean mute) throws RestApiException; - - /** - * Check if this change is muted. - * - * @return true if the change is muted. - */ - boolean muted() throws RestApiException; + void markAsReviewed(boolean reviewed) throws RestApiException; /** * Create a new change that reverts this change. @@ -583,12 +577,7 @@ } @Override - public void mute(boolean mute) throws RestApiException { - throw new NotImplementedException(); - } - - @Override - public boolean muted() throws RestApiException { + public void markAsReviewed(boolean reviewed) throws RestApiException { throw new NotImplementedException(); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java index a411e0e..3cde570 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
@@ -23,6 +23,8 @@ DashboardInfo get(boolean inherited) throws RestApiException; + void setDefault() throws RestApiException; + /** * A default implementation which allows source compatibility when adding new methods to the * interface. @@ -37,5 +39,10 @@ public DashboardInfo get(boolean inherited) throws RestApiException { throw new NotImplementedException(); } + + @Override + public void setDefault() throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index a61b68a..8320ef7 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -155,6 +155,22 @@ DashboardApi defaultDashboard() throws RestApiException; /** + * Set the project's default dashboard. + * + * @param name the dashboard to set as default. + */ + void defaultDashboard(String name) throws RestApiException; + + /** Remove the project's default dashboard. */ + void removeDefaultDashboard() throws RestApiException; + + abstract class ListDashboardsRequest { + public abstract List<DashboardInfo> get() throws RestApiException; + } + + ListDashboardsRequest dashboards() throws RestApiException; + + /** * A default implementation which allows source compatibility when adding new methods to the * interface. */ @@ -273,5 +289,20 @@ public DashboardApi defaultDashboard() throws RestApiException { throw new NotImplementedException(); } + + @Override + public ListDashboardsRequest dashboards() throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void defaultDashboard(String name) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void removeDefaultDashboard() throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java index 706482f..f802049 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -37,7 +37,6 @@ public Timestamp submitted; public AccountInfo submitter; public Boolean starred; - public Boolean muted; public Collection<String> stars; public Boolean reviewed; public SubmitType submitType;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java index 5ef3dce..866d74f 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -138,8 +138,6 @@ public final native boolean starred() /*-{ return this.starred ? true : false; }-*/; - public final native boolean muted() /*-{ return this.muted ? true : false; }-*/; - public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/; public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java index f073acf..72dcfb9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -234,13 +234,16 @@ static class UploadFilter implements Filter { private final VisibleRefFilter.Factory refFilterFactory; private final UploadValidators.Factory uploadValidatorsFactory; + private final PermissionBackend permissionBackend; @Inject UploadFilter( VisibleRefFilter.Factory refFilterFactory, - UploadValidators.Factory uploadValidatorsFactory) { + UploadValidators.Factory uploadValidatorsFactory, + PermissionBackend permissionBackend) { this.refFilterFactory = refFilterFactory; this.uploadValidatorsFactory = uploadValidatorsFactory; + this.permissionBackend = permissionBackend; } @Override @@ -251,13 +254,20 @@ ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL); UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER); - if (!pc.canRunUploadPack()) { + try { + permissionBackend + .user(pc.getUser()) + .project(pc.getProject().getNameKey()) + .check(ProjectPermission.RUN_UPLOAD_PACK); + } catch (AuthException e) { GitSmartHttpTools.sendError( (HttpServletRequest) request, (HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN, "upload-pack not permitted on this server"); return; + } catch (PermissionBackendException e) { + throw new ServletException(e); } // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR // may have been overridden by a proxy server -- we'll try to avoid this. @@ -312,10 +322,14 @@ static class ReceiveFilter implements Filter { private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache; + private final PermissionBackend permissionBackend; @Inject - ReceiveFilter(@Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache) { + ReceiveFilter( + @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache, + PermissionBackend permissionBackend) { this.cache = cache; + this.permissionBackend = permissionBackend; } @Override @@ -329,13 +343,20 @@ ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL); Project.NameKey projectName = pc.getProject().getNameKey(); - if (!pc.canRunReceivePack()) { + try { + permissionBackend + .user(pc.getUser()) + .project(pc.getProject().getNameKey()) + .check(ProjectPermission.RUN_RECEIVE_PACK); + } catch (AuthException e) { GitSmartHttpTools.sendError( (HttpServletRequest) request, (HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN, "receive-pack not permitted on this server"); return; + } catch (PermissionBackendException e) { + throw new RuntimeException(e); } Capable s = arc.canUpload();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java index 0ee720a..9940cd9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,7 +21,6 @@ import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.config.AuthConfig; @@ -30,7 +29,6 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.servlet.ServletModule; import java.io.IOException; @@ -59,7 +57,6 @@ } } - private final Provider<ReviewDb> db; private final boolean enabled; private final DynamicItem<WebSession> session; private final PermissionBackend permissionBackend; @@ -67,12 +64,10 @@ @Inject RunAsFilter( - Provider<ReviewDb> db, AuthConfig config, DynamicItem<WebSession> session, PermissionBackend permissionBackend, AccountResolver accountResolver) { - this.db = db; this.enabled = config.isRunAsEnabled(); this.session = session; this.permissionBackend = permissionBackend; @@ -111,7 +106,7 @@ Account target; try { - target = accountResolver.find(db.get(), runas); + target = accountResolver.find(runas); } catch (OrmException | IOException | ConfigInvalidException e) { log.warn("cannot resolve account for " + RUN_AS, e); replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java index 2f3d32f..c508b2d 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
@@ -15,11 +15,17 @@ package com.google.gerrit.httpd.raw; import com.google.common.cache.Cache; +import com.google.gerrit.common.TimeUtil; +import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; class PolyGerritUiServlet extends ResourceServlet { private static final long serialVersionUID = 1L; + private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs()); + private final Path ui; PolyGerritUiServlet(Cache<Path, Resource> cache, Path ui) { @@ -31,4 +37,16 @@ protected Path getResourcePath(String pathInfo) { return ui.resolve(pathInfo); } + + @Override + protected FileTime getLastModifiedTime(Path p) throws IOException { + if (ui.getFileSystem().equals(FileSystems.getDefault())) { + // Assets are being served from disk, so we can trust the mtime. + return super.getLastModifiedTime(p); + } + // Assume this FileSystem is serving from a WAR. All WAR outputs from the build process have + // mtimes of 1980/1/1, so we can't trust it, and return the initialization time of this class + // instead. + return NOW; + } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java index 150acc6..94ee221 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -31,7 +31,6 @@ import com.google.common.cache.Cache; import com.google.common.collect.ImmutableMap; import com.google.common.hash.Hashing; -import com.google.gerrit.common.FileUtil; import com.google.gerrit.common.Nullable; import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gwtexpui.server.CacheHeaders; @@ -252,7 +251,7 @@ return true; } - long lastModified = FileUtil.lastModified(p); + long lastModified = getLastModifiedTime(p).toMillis(); if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) { rsp.setStatus(SC_NOT_MODIFIED); return true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java index 93bd5ae..3f6ff25 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
@@ -15,12 +15,16 @@ package com.google.gerrit.httpd.raw; import com.google.common.cache.Cache; +import com.google.gerrit.common.TimeUtil; import java.nio.file.FileSystem; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; class WarDocServlet extends ResourceServlet { private static final long serialVersionUID = 1L; + private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs()); + private final FileSystem warFs; WarDocServlet(Cache<Path, Resource> cache, FileSystem warFs) { @@ -32,4 +36,11 @@ protected Path getResourcePath(String pathInfo) { return warFs.getPath("/Documentation/" + pathInfo); } + + @Override + protected FileTime getLastModifiedTime(Path p) { + // Return initialization time of this class, since the WAR outputs from the build process all + // have mtimes of 1980/1/1. + return NOW; + } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java index 0e6ae84..2adf029 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -25,6 +25,7 @@ import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; @@ -62,6 +63,7 @@ AllProjectsName allProjects, Provider<SetParent> setParent, GitReferenceUpdated gitRefUpdated, + ContributorAgreementsChecker contributorAgreements, @Assisted("projectName") Project.NameKey projectName, @Nullable @Assisted ObjectId base, @Assisted List<AccessSection> sectionList, @@ -78,6 +80,7 @@ sectionList, parentProjectName, message, + contributorAgreements, true); this.projectAccessFactory = projectAccessFactory; this.projectCache = projectCache;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java index ecbcb39..3fa05ab 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -18,7 +18,6 @@ import com.google.common.base.MoreObjects; import com.google.gerrit.common.data.AccessSection; -import com.google.gerrit.common.data.Capable; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRule; @@ -37,6 +36,7 @@ import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.RefPattern; @@ -58,6 +58,7 @@ private final MetaDataUpdate.User metaDataUpdateFactory; private final AllProjectsName allProjects; private final Provider<SetParent> setParent; + private final ContributorAgreementsChecker contributorAgreements; protected final Project.NameKey projectName; protected final ObjectId base; @@ -77,6 +78,7 @@ List<AccessSection> sectionList, Project.NameKey parentProjectName, String message, + ContributorAgreementsChecker contributorAgreements, boolean checkIfOwner) { this.projectControlFactory = projectControlFactory; this.groupBackend = groupBackend; @@ -89,6 +91,7 @@ this.sectionList = sectionList; this.parentProjectName = parentProjectName; this.message = message; + this.contributorAgreements = contributorAgreements; this.checkIfOwner = checkIfOwner; } @@ -99,9 +102,10 @@ PermissionDeniedException, PermissionBackendException { final ProjectControl projectControl = projectControlFactory.controlFor(projectName); - Capable r = projectControl.canPushToAtLeastOneRef(); - if (r != Capable.OK) { - throw new PermissionDeniedException(r.getMessage()); + try { + contributorAgreements.check(projectName, projectControl.getUser()); + } catch (AuthException e) { + throw new PermissionDeniedException(e.getMessage()); } try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java index 4e2a4d3..f27b9d3 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -43,6 +43,7 @@ import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.RefPermission; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.SetParent; @@ -94,6 +95,7 @@ BatchUpdate.Factory updateFactory, Provider<SetParent> setParent, Sequences seq, + ContributorAgreementsChecker contributorAgreements, @Assisted("projectName") Project.NameKey projectName, @Nullable @Assisted ObjectId base, @Assisted List<AccessSection> sectionList, @@ -110,6 +112,7 @@ sectionList, parentProjectName, message, + contributorAgreements, false); this.db = db; this.permissionBackend = permissionBackend;
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy index 51c60af..c3b522a 100644 --- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy +++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -21,7 +21,7 @@ * @param staticResourcePath * @param? versionInfo */ -{template .Index autoescape="strict" kind="html"} +{template .Index kind="html"} <!DOCTYPE html>{\n} <html lang="en">{\n} <meta charset="utf-8">{\n}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index a71a7fae..6b5c157 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -51,6 +51,7 @@ import com.google.gerrit.pgm.util.SiteProgram; import com.google.gerrit.server.LibModuleLoader; import com.google.gerrit.server.StartupChecks; +import com.google.gerrit.server.account.AccountDeactivator; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -461,6 +462,7 @@ }); modules.add(new GarbageCollectionModule()); if (!slave) { + modules.add(new AccountDeactivator.Module()); modules.add(new ChangeCleanupRunner.Module()); } modules.addAll(LibModuleLoader.loadModules(cfgInjector));
diff --git a/fake_pom.xml b/gerrit-plugin-api/pom.xml similarity index 85% copy from fake_pom.xml copy to gerrit-plugin-api/pom.xml index 6ec45e5..84df44a 100644 --- a/fake_pom.xml +++ b/gerrit-plugin-api/pom.xml
@@ -1,11 +1,11 @@ <project> <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> - <artifactId>gerrit</artifactId> - <version>1</version> <!-- Do not edit; see version.bzl. --> + <artifactId>gerrit-plugin-api</artifactId> + <version>2.16-SNAPSHOT</version> <packaging>jar</packaging> - <name>Gerrit Code Review - Extension API</name> - <description>API for Gerrit Extensions</description> + <name>Gerrit Code Review - Plugin API</name> + <description>API for Gerrit Plugins</description> <url>https://www.gerritcodereview.com/</url> <licenses> @@ -53,15 +53,9 @@ <name>Logan Hanks</name> </developer> <developer> - <name>Luca Milanesio</name> - </developer> - <developer> <name>Martin Fick</name> </developer> <developer> - <name>Patrick Hiesel</name> - </developer> - <developer> <name>Saša Živkov</name> </developer> <developer>
diff --git a/fake_pom.xml b/gerrit-plugin-gwtui/pom.xml similarity index 85% copy from fake_pom.xml copy to gerrit-plugin-gwtui/pom.xml index 6ec45e5..cc9aafc 100644 --- a/fake_pom.xml +++ b/gerrit-plugin-gwtui/pom.xml
@@ -1,11 +1,11 @@ <project> <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> - <artifactId>gerrit</artifactId> - <version>1</version> <!-- Do not edit; see version.bzl. --> + <artifactId>gerrit-plugin-gwtui</artifactId> + <version>2.16-SNAPSHOT</version> <packaging>jar</packaging> - <name>Gerrit Code Review - Extension API</name> - <description>API for Gerrit Extensions</description> + <name>Gerrit Code Review - Plugin GWT UI</name> + <description>Common Classes for Gerrit GWT UI Plugins</description> <url>https://www.gerritcodereview.com/</url> <licenses> @@ -53,15 +53,9 @@ <name>Logan Hanks</name> </developer> <developer> - <name>Luca Milanesio</name> - </developer> - <developer> <name>Martin Fick</name> </developer> <developer> - <name>Patrick Hiesel</name> - </developer> - <developer> <name>Saša Živkov</name> </developer> <developer>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java index 4854112..3759f09 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
@@ -18,10 +18,16 @@ import com.google.gerrit.server.plugins.DelegatingClassLoader; import com.google.gerrit.util.cli.CmdLineParser; import com.google.inject.Injector; +import com.google.inject.Module; import com.google.inject.Provider; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.WeakHashMap; /** Helper class to define and parse options from plugins on ssh and RestAPI commands. */ public class DynamicOptions { @@ -50,6 +56,64 @@ public interface DynamicBean {} /** + * To provide additional options to a command in another classloader, bind a ClassNameProvider + * which provides the name of your DynamicBean in the other classLoader. + * + * <p>Do this by binding to just the name of the command you are going to bind to so that your + * classLoader does not load the command's class which likely is not in your classpath. To ensure + * that the command's class is not in your classpath, you can exclude it during your build. + * + * <p>For example: + * + * <pre> + * bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class) + * .annotatedWith(Exports.named( "com.google.gerrit.plugins.otherplugin.command")) + * .to(MyOptionsClassNameProvider.class); + * + * static class MyOptionsClassNameProvider implements DynamicOptions.ClassNameProvider { + * @Override + * public String getClassName() { + * return "com.googlesource.gerrit.plugins.myplugin.CommandOptions"; + * } + * } + * </pre> + */ + public interface ClassNameProvider extends DynamicBean { + String getClassName(); + } + + /** + * To provide additional Guice bindings for options to a command in another classloader, bind a + * ModulesClassNamesProvider which provides the name of your Modules needed for your DynamicBean + * in the other classLoader. + * + * <p>Do this by binding to the name of the command you are going to bind to and providing an + * Iterable of Module names to instantiate and add to the Injector used to instantiate the + * DynamicBean in the other classLoader. For example: + * + * <pre> + * bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class) + * .annotatedWith(Exports.named( + * "com.google.gerrit.plugins.otherplugin.command")) + * .to(MyOptionsModulesClassNamesProvider.class); + * + * static class MyOptionsModulesClassNamesProvider implements DynamicOptions.ClassNameProvider { + * @Override + * public String getClassName() { + * return "com.googlesource.gerrit.plugins.myplugin.CommandOptions"; + * } + * @Override + * public Iterable<String> getModulesClassNames()() { + * return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule"; + * } + * } + * </pre> + */ + public interface ModulesClassNamesProvider extends ClassNameProvider { + Iterable<String> getModulesClassNames(); + } + + /** * Implement this if your DynamicBean needs an opportunity to act on the Bean directly before or * after argument parsing. */ @@ -82,6 +146,24 @@ void setDynamicBean(String plugin, DynamicBean dynamicBean); } + /** + * MergedClassloaders allow us to load classes from both plugin classloaders. Store the merged + * classloaders in a Map to avoid creating a new classloader for each invocation. Use a + * WeakHashMap to avoid leaking these MergedClassLoaders once either plugin is unloaded. Since the + * WeakHashMap only takes care of ensuring the Keys can get garbage collected, use WeakReferences + * to store the MergedClassloaders in the WeakHashMap. + * + * <p>Outter keys are the bean plugin's classloaders (the plugin being extended) + * + * <p>Inner keys are the dynamicBeans plugin's classloaders (the extending plugin) + * + * <p>The value is the MergedClassLoader representing the merging of the outter and inner key + * classloaders. + */ + protected static Map<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>> mergedClByCls = + Collections.synchronizedMap( + new WeakHashMap<ClassLoader, Map<ClassLoader, WeakReference<ClassLoader>>>()); + protected Object bean; protected Map<String, DynamicBean> beansByPlugin; protected Injector injector; @@ -118,24 +200,63 @@ public DynamicBean getDynamicBean(Object bean, DynamicBean dynamicBean) { ClassLoader coreCl = getClass().getClassLoader(); ClassLoader beanCl = bean.getClass().getClassLoader(); + + ClassLoader loader = beanCl; if (beanCl != coreCl) { // bean from a plugin? ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader(); if (beanCl != dynamicBeanCl) { // in a different plugin? - ClassLoader mergedCL = new DelegatingClassLoader(beanCl, dynamicBeanCl); - try { - return injector - .createChildInjector() - .getInstance( - (Class<DynamicOptions.DynamicBean>) - mergedCL.loadClass(dynamicBean.getClass().getCanonicalName())); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } + loader = getMergedClassLoader(beanCl, dynamicBeanCl); } } + + String className = null; + if (dynamicBean instanceof ClassNameProvider) { + className = ((ClassNameProvider) dynamicBean).getClassName(); + } else if (loader != beanCl) { // in a different plugin? + className = dynamicBean.getClass().getCanonicalName(); + } + + if (className != null) { + try { + List<Module> modules = new ArrayList<>(); + Injector modulesInjector = injector; + if (dynamicBean instanceof ModulesClassNamesProvider) { + modulesInjector = injector.createChildInjector(); + for (String moduleName : + ((ModulesClassNamesProvider) dynamicBean).getModulesClassNames()) { + Class<Module> mClass = (Class<Module>) loader.loadClass(moduleName); + modules.add(modulesInjector.getInstance(mClass)); + } + } + return modulesInjector + .createChildInjector(modules) + .getInstance((Class<DynamicOptions.DynamicBean>) loader.loadClass(className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + return dynamicBean; } + protected ClassLoader getMergedClassLoader(ClassLoader beanCl, ClassLoader dynamicBeanCl) { + Map<ClassLoader, WeakReference<ClassLoader>> mergedClByCl = mergedClByCls.get(beanCl); + if (mergedClByCl == null) { + mergedClByCl = Collections.synchronizedMap(new WeakHashMap<>()); + mergedClByCls.put(beanCl, mergedClByCl); + } + WeakReference<ClassLoader> mergedClRef = mergedClByCl.get(dynamicBeanCl); + ClassLoader mergedCl = null; + if (mergedClRef != null) { + mergedCl = mergedClRef.get(); + } + if (mergedCl == null) { + mergedCl = new DelegatingClassLoader(beanCl, dynamicBeanCl); + mergedClByCl.put(dynamicBeanCl, new WeakReference<>(mergedCl)); + } + return mergedCl; + } + public void parseDynamicBeans(CmdLineParser clp) { for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) { clp.parseWithPrefix("--" + e.getKey(), e.getValue());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java index 13c24e0..a8cc0f4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; import com.google.common.base.CharMatcher; @@ -26,6 +27,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; @@ -49,6 +51,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -154,7 +157,8 @@ public static final String DEFAULT_LABEL = "star"; public static final String IGNORE_LABEL = "ignore"; - public static final String MUTE_LABEL = "mute"; + public static final String REVIEWED_LABEL = "reviewed"; + public static final String UNREVIEWED_LABEL = "unreviewed"; public static final ImmutableSortedSet<String> DEFAULT_LABELS = ImmutableSortedSet.of(DEFAULT_LABEL); @@ -330,37 +334,41 @@ return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId()); } - private static String getMuteLabel(Change change) { - return MUTE_LABEL + "/" + change.currentPatchSetId().get(); + private static String getReviewedLabel(Change change) { + return getReviewedLabel(change.currentPatchSetId().get()); } - public void mute(ChangeResource rsrc) throws OrmException, IllegalLabelException { + private static String getReviewedLabel(int ps) { + return REVIEWED_LABEL + "/" + ps; + } + + private static String getUnreviewedLabel(Change change) { + return getUnreviewedLabel(change.currentPatchSetId().get()); + } + + private static String getUnreviewedLabel(int ps) { + return UNREVIEWED_LABEL + "/" + ps; + } + + public void markAsReviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException { star( rsrc.getUser().asIdentifiedUser().getAccountId(), rsrc.getProject(), rsrc.getChange().getId(), - ImmutableSet.of(getMuteLabel(rsrc.getChange())), - ImmutableSet.of()); + ImmutableSet.of(getReviewedLabel(rsrc.getChange())), + ImmutableSet.of(getUnreviewedLabel(rsrc.getChange()))); } - public void unmute(ChangeResource rsrc) throws OrmException, IllegalLabelException { + public void markAsUnreviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException { star( rsrc.getUser().asIdentifiedUser().getAccountId(), rsrc.getProject(), rsrc.getChange().getId(), - ImmutableSet.of(), - ImmutableSet.of(getMuteLabel(rsrc.getChange()))); + ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())), + ImmutableSet.of(getReviewedLabel(rsrc.getChange()))); } - public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException { - return getLabels(accountId, change.getId()).contains(getMuteLabel(change)); - } - - public boolean isMuted(ChangeResource rsrc) throws OrmException { - return isMutedBy(rsrc.getChange(), rsrc.getUser().asIdentifiedUser().getAccountId()); - } - - private static StarRef readLabels(Repository repo, String refName) throws IOException { + public static StarRef readLabels(Repository repo, String refName) throws IOException { Ref ref = repo.exactRef(refName); if (ref == null) { return StarRef.MISSING; @@ -394,6 +402,25 @@ if (labels.containsAll(ImmutableSet.of(DEFAULT_LABEL, IGNORE_LABEL))) { throw new MutuallyExclusiveLabelsException(DEFAULT_LABEL, IGNORE_LABEL); } + + Set<Integer> reviewedPatchSets = + labels + .stream() + .filter(l -> l.startsWith(REVIEWED_LABEL)) + .map(l -> Integer.valueOf(l.substring(REVIEWED_LABEL.length() + 1))) + .collect(toSet()); + Set<Integer> unreviewedPatchSets = + labels + .stream() + .filter(l -> l.startsWith(UNREVIEWED_LABEL)) + .map(l -> Integer.valueOf(l.substring(UNREVIEWED_LABEL.length() + 1))) + .collect(toSet()); + Optional<Integer> ps = + Sets.intersection(reviewedPatchSets, unreviewedPatchSets).stream().findFirst(); + if (ps.isPresent()) { + throw new MutuallyExclusiveLabelsException( + getReviewedLabel(ps.get()), getUnreviewedLabel(ps.get())); + } } private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java new file mode 100644 index 0000000..c222756 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -0,0 +1,121 @@ +// 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. + +package com.google.gerrit.server.account; + +import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.ScheduleConfig; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.query.account.AccountPredicates; +import com.google.gerrit.server.query.account.InternalAccountQuery; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Runnable to enable scheduling account deactivations to run periodically */ +public class AccountDeactivator implements Runnable { + private static final Logger log = LoggerFactory.getLogger(AccountDeactivator.class); + + public static class Module extends LifecycleModule { + @Override + protected void configure() { + listener().to(Lifecycle.class); + } + } + + static class Lifecycle implements LifecycleListener { + private final WorkQueue queue; + private final AccountDeactivator deactivator; + private final boolean supportAutomaticAccountActivityUpdate; + private final ScheduleConfig scheduleConfig; + + @Inject + Lifecycle(WorkQueue queue, AccountDeactivator deactivator, @GerritServerConfig Config cfg) { + this.queue = queue; + this.deactivator = deactivator; + scheduleConfig = new ScheduleConfig(cfg, "accountDeactivation"); + supportAutomaticAccountActivityUpdate = + cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false); + } + + @Override + public void start() { + if (!supportAutomaticAccountActivityUpdate) { + return; + } + long interval = scheduleConfig.getInterval(); + long delay = scheduleConfig.getInitialDelay(); + if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) { + log.info("Ignoring missing accountDeactivator schedule configuration"); + } else if (delay < 0 || interval <= 0) { + log.warn( + String.format( + "Ignoring invalid accountDeactivator schedule configuration: %s", scheduleConfig)); + } else { + queue + .getDefaultQueue() + .scheduleAtFixedRate(deactivator, delay, interval, TimeUnit.MILLISECONDS); + } + } + + @Override + public void stop() { + // handled by WorkQueue.stop() already + } + } + + private final Provider<InternalAccountQuery> accountQueryProvider; + private final Realm realm; + private final SetInactiveFlag sif; + + @Inject + AccountDeactivator( + Provider<InternalAccountQuery> accountQueryProvider, SetInactiveFlag sif, Realm realm) { + this.accountQueryProvider = accountQueryProvider; + this.sif = sif; + this.realm = realm; + } + + @Override + public void run() { + log.debug("Running account deactivations"); + try { + int numberOfAccountsDeactivated = 0; + for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) { + log.debug("processing account " + acc.getUserName()); + if (acc.getUserName() != null && !realm.isActive(acc.getUserName())) { + sif.deactivate(acc.getAccount().getId()); + log.debug("deactivated accout " + acc.getUserName()); + numberOfAccountsDeactivated++; + } + } + log.info( + "Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated); + } catch (Exception e) { + log.error("Failed to deactivate inactive accounts " + e.getMessage(), e); + } + } + + @Override + public String toString() { + return "account deactivator"; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java index ec756bc..046b60a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -22,6 +22,8 @@ import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -70,6 +72,8 @@ private final ExternalIds externalIds; private final ExternalIdsUpdate.Server externalIdsUpdateFactory; private final GroupsUpdate.Factory groupsUpdateFactory; + private final boolean autoUpdateAccountActiveStatus; + private final SetInactiveFlag setInactiveFlag; @Inject AccountManager( @@ -86,7 +90,8 @@ Provider<InternalAccountQuery> accountQueryProvider, ExternalIds externalIds, ExternalIdsUpdate.Server externalIdsUpdateFactory, - GroupsUpdate.Factory groupsUpdateFactory) { + GroupsUpdate.Factory groupsUpdateFactory, + SetInactiveFlag setInactiveFlag) { this.schema = schema; this.sequences = sequences; this.accounts = accounts; @@ -102,6 +107,9 @@ this.externalIds = externalIds; this.externalIdsUpdateFactory = externalIdsUpdateFactory; this.groupsUpdateFactory = groupsUpdateFactory; + this.autoUpdateAccountActiveStatus = + cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false); + this.setInactiveFlag = setInactiveFlag; } /** @return user identified by this external identity string */ @@ -122,8 +130,8 @@ * @param who identity of the user, with any details we received about them. * @return the result of authenticating the user. * @throws AccountException the account does not exist, and cannot be created, or exists, but - * cannot be located, or is inactive, or cannot be added to the admin group (only for the - * first account). + * cannot be located, is unable to be activated or deactivated, or is inactive, or cannot be + * added to the admin group (only for the first account). */ public AuthResult authenticate(AuthRequest who) throws AccountException, IOException { who = realm.authenticate(who); @@ -138,6 +146,24 @@ // Account exists Account act = byIdCache.get(id.accountId()).getAccount(); + if (autoUpdateAccountActiveStatus && who.authProvidesAccountActiveStatus()) { + if (who.isActive() && !act.isActive()) { + try { + setInactiveFlag.activate(act.getId()); + act = byIdCache.get(id.accountId()).getAccount(); + } catch (ResourceNotFoundException e) { + throw new AccountException("Unable to activate account " + act.getId(), e); + } + } else if (!who.isActive() && act.isActive()) { + try { + setInactiveFlag.deactivate(act.getId()); + act = byIdCache.get(id.accountId()).getAccount(); + } catch (RestApiException e) { + throw new AccountException("Unable to deactivate account " + act.getId(), e); + } + } + } + if (!act.isActive()) { throw new AccountException("Authentication error, account inactive"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java index 94f63a7..a1cca06 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -17,7 +17,6 @@ import static java.util.stream.Collectors.toSet; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -63,9 +62,8 @@ * @return the single account that matches; null if no account matches or there are multiple * candidates. */ - public Account find(ReviewDb db, String nameOrEmail) - throws OrmException, IOException, ConfigInvalidException { - Set<Account.Id> r = findAll(db, nameOrEmail); + public Account find(String nameOrEmail) throws OrmException, IOException, ConfigInvalidException { + Set<Account.Id> r = findAll(nameOrEmail); if (r.size() == 1) { return byId.get(r.iterator().next()).getAccount(); } @@ -87,13 +85,12 @@ /** * Find all accounts matching the name or name/email string. * - * @param db open database handle. * @param nameOrEmail a string of the format "Full Name <email@example>", just the email * address ("email@example"), a full name ("Full Name"), an account id ("18419") or an user * name ("username"). * @return the accounts that match, empty collection if none. Never null. */ - public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail) + public Set<Account.Id> findAll(String nameOrEmail) throws OrmException, IOException, ConfigInvalidException { Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail); if (m.matches()) { @@ -119,34 +116,30 @@ } } - return findAllByNameOrEmail(db, nameOrEmail); + return findAllByNameOrEmail(nameOrEmail); } /** * Locate exactly one account matching the name or name/email string. * - * @param db open database handle. * @param nameOrEmail a string of the format "Full Name <email@example>", just the email * address ("email@example"), a full name ("Full Name"). * @return the single account that matches; null if no account matches or there are multiple * candidates. */ - public Account findByNameOrEmail(ReviewDb db, String nameOrEmail) - throws OrmException, IOException { - Set<Account.Id> r = findAllByNameOrEmail(db, nameOrEmail); + public Account findByNameOrEmail(String nameOrEmail) throws OrmException, IOException { + Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail); return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null; } /** * Locate exactly one account matching the name or name/email string. * - * @param db open database handle. * @param nameOrEmail a string of the format "Full Name <email@example>", just the email * address ("email@example"), a full name ("Full Name"). * @return the accounts that match, empty collection if none. Never null. */ - public Set<Account.Id> findAllByNameOrEmail(ReviewDb db, String nameOrEmail) - throws OrmException, IOException { + public Set<Account.Id> findAllByNameOrEmail(String nameOrEmail) throws OrmException, IOException { int lt = nameOrEmail.indexOf('<'); int gt = nameOrEmail.indexOf('>'); if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java index dcda816..19a8259 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -25,7 +25,6 @@ import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; @@ -39,7 +38,6 @@ @Singleton public class AccountsCollection implements RestCollection<TopLevelResource, AccountResource>, AcceptsCreate<TopLevelResource> { - private final Provider<ReviewDb> db; private final Provider<CurrentUser> self; private final AccountResolver resolver; private final AccountControl.Factory accountControlFactory; @@ -50,7 +48,6 @@ @Inject AccountsCollection( - Provider<ReviewDb> db, Provider<CurrentUser> self, AccountResolver resolver, AccountControl.Factory accountControlFactory, @@ -58,7 +55,6 @@ Provider<QueryAccounts> list, DynamicMap<RestView<AccountResource>> views, CreateAccount.Factory createAccountFactory) { - this.db = db; this.self = self; this.resolver = resolver; this.accountControlFactory = accountControlFactory; @@ -144,7 +140,7 @@ } } - Account match = resolver.find(db.get(), id); + Account match = resolver.find(id); if (match == null) { return null; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java index e654b8d..6647ca4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -63,6 +63,8 @@ private boolean skipAuthentication; private String authPlugin; private String authProvider; + private boolean authProvidesAccountActiveStatus; + private boolean active; public AuthRequest(ExternalId.Key externalId) { this.externalId = externalId; @@ -140,4 +142,20 @@ public void setAuthProvider(String authProvider) { this.authProvider = authProvider; } + + public boolean authProvidesAccountActiveStatus() { + return authProvidesAccountActiveStatus; + } + + public void setAuthProvidesAccountActiveStatus(boolean authProvidesAccountActiveStatus) { + this.authProvidesAccountActiveStatus = authProvidesAccountActiveStatus; + } + + public boolean isActive() { + return active; + } + + public void setActive(Boolean isActive) { + this.active = isActive; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java index 0ff5342..43669c0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -49,6 +49,6 @@ if (self.get() == rsrc.getUser()) { throw new ResourceConflictException("cannot deactivate own account"); } - return setInactiveFlag.deactivate(rsrc.getUser()); + return setInactiveFlag.deactivate(rsrc.getUser().getAccountId()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java index 825ef10..7ce2ea8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -41,6 +41,6 @@ @Override public Response<String> apply(AccountResource rsrc, Input input) throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException { - return setInactiveFlag.activate(rsrc.getUser()); + return setInactiveFlag.activate(rsrc.getUser().getAccountId()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java index b5e4cba..c375dd6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -19,6 +19,8 @@ import com.google.gerrit.server.IdentifiedUser; import java.io.IOException; import java.util.Set; +import javax.naming.NamingException; +import javax.security.auth.login.LoginException; public interface Realm { /** Can the end-user modify this field of their own account? */ @@ -45,4 +47,15 @@ * into an email address, and then locate the user by that email address. */ Account.Id lookup(String accountName) throws IOException; + + /** + * @return true if the account is active. + * @throws NamingException + * @throws LoginException + * @throws AccountException + */ + default boolean isActive(@SuppressWarnings("unused") String username) + throws LoginException, NamingException, AccountException { + return true; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java index 1698387..6e12c3e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -19,7 +19,6 @@ import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.IdentifiedUser; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -36,14 +35,14 @@ this.accountsUpdate = accountsUpdate; } - public Response<?> deactivate(IdentifiedUser user) + public Response<?> deactivate(Account.Id accountId) throws RestApiException, IOException, ConfigInvalidException { AtomicBoolean alreadyInactive = new AtomicBoolean(false); Account account = accountsUpdate .create() .update( - user.getAccountId(), + accountId, a -> { if (!a.isActive()) { alreadyInactive.set(true); @@ -60,14 +59,14 @@ return Response.none(); } - public Response<String> activate(IdentifiedUser user) + public Response<String> activate(Account.Id accountId) throws ResourceNotFoundException, IOException, ConfigInvalidException { AtomicBoolean alreadyActive = new AtomicBoolean(false); Account account = accountsUpdate .create() .update( - user.getAccountId(), + accountId, a -> { if (a.isActive()) { alreadyActive.set(true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java index d43327f..0fba74a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -69,8 +69,9 @@ import com.google.gerrit.server.change.ListChangeComments; import com.google.gerrit.server.change.ListChangeDrafts; import com.google.gerrit.server.change.ListChangeRobotComments; +import com.google.gerrit.server.change.MarkAsReviewed; +import com.google.gerrit.server.change.MarkAsUnreviewed; import com.google.gerrit.server.change.Move; -import com.google.gerrit.server.change.Mute; import com.google.gerrit.server.change.PostHashtags; import com.google.gerrit.server.change.PostPrivate; import com.google.gerrit.server.change.PostReviewers; @@ -88,7 +89,6 @@ import com.google.gerrit.server.change.SubmittedTogether; import com.google.gerrit.server.change.SuggestChangeReviewers; import com.google.gerrit.server.change.Unignore; -import com.google.gerrit.server.change.Unmute; import com.google.gerrit.server.change.WorkInProgressOp; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -140,8 +140,8 @@ private final DeletePrivate deletePrivate; private final Ignore ignore; private final Unignore unignore; - private final Mute mute; - private final Unmute unmute; + private final MarkAsReviewed markAsReviewed; + private final MarkAsUnreviewed markAsUnreviewed; private final SetWorkInProgress setWip; private final SetReadyForReview setReady; private final PutMessage putMessage; @@ -185,8 +185,8 @@ DeletePrivate deletePrivate, Ignore ignore, Unignore unignore, - Mute mute, - Unmute unmute, + MarkAsReviewed markAsReviewed, + MarkAsUnreviewed markAsUnreviewed, SetWorkInProgress setWip, SetReadyForReview setReady, PutMessage putMessage, @@ -228,8 +228,8 @@ this.deletePrivate = deletePrivate; this.ignore = ignore; this.unignore = unignore; - this.mute = mute; - this.unmute = unmute; + this.markAsReviewed = markAsReviewed; + this.markAsUnreviewed = markAsUnreviewed; this.setWip = setWip; this.setReady = setReady; this.putMessage = putMessage; @@ -677,26 +677,18 @@ } @Override - public void mute(boolean mute) throws RestApiException { + public void markAsReviewed(boolean reviewed) throws RestApiException { // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into // StarredChangesUtil. try { - if (mute) { - this.mute.apply(change, new Mute.Input()); + if (reviewed) { + markAsReviewed.apply(change, new MarkAsReviewed.Input()); } else { - unmute.apply(change, new Unmute.Input()); + markAsUnreviewed.apply(change, new MarkAsUnreviewed.Input()); } } catch (OrmException | IllegalLabelException e) { - throw asRestApiException("Cannot mute change", e); - } - } - - @Override - public boolean muted() throws RestApiException { - try { - return stars.isMuted(change); - } catch (OrmException e) { - throw asRestApiException("Cannot check if muted", e); + throw asRestApiException( + "Cannot mark change as " + (reviewed ? "reviewed" : "unreviewed"), e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java index 58cb59e..0d4afd6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -16,8 +16,10 @@ import static com.google.gerrit.server.api.ApiUtil.asRestApiException; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.api.projects.DashboardApi; import com.google.gerrit.extensions.api.projects.DashboardInfo; +import com.google.gerrit.extensions.common.SetDashboardInput; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -26,6 +28,7 @@ import com.google.gerrit.server.project.DashboardsCollection; import com.google.gerrit.server.project.GetDashboard; import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.project.SetDashboard; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; @@ -39,6 +42,7 @@ private final DashboardsCollection dashboards; private final Provider<GetDashboard> get; + private final SetDashboard set; private final ProjectResource project; private final String id; @@ -46,10 +50,12 @@ DashboardApiImpl( DashboardsCollection dashboards, Provider<GetDashboard> get, + SetDashboard set, @Assisted ProjectResource project, - @Assisted String id) { + @Assisted @Nullable String id) { this.dashboards = dashboards; this.get = get; + this.set = set; this.project = project; this.id = id; } @@ -68,6 +74,18 @@ } } + @Override + public void setDefault() throws RestApiException { + SetDashboardInput input = new SetDashboardInput(); + input.id = id; + try { + set.apply(DashboardResource.projectDefault(project.getControl()), input); + } catch (Exception e) { + String msg = String.format("Cannot %s default dashboard", id != null ? "set" : "remove"); + throw asRestApiException(msg, e); + } + } + private DashboardResource resource() throws ResourceNotFoundException, IOException, ConfigInvalidException, PermissionBackendException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index 5012280..9fd4d48 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -16,6 +16,7 @@ import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import static com.google.gerrit.server.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME; +import static java.util.stream.Collectors.toList; import com.google.gerrit.extensions.api.access.ProjectAccessInfo; import com.google.gerrit.extensions.api.access.ProjectAccessInput; @@ -28,6 +29,7 @@ import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.DashboardApi; +import com.google.gerrit.extensions.api.projects.DashboardInfo; import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; import com.google.gerrit.extensions.api.projects.DeleteTagsInput; import com.google.gerrit.extensions.api.projects.DescriptionInput; @@ -39,6 +41,7 @@ import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -58,6 +61,7 @@ import com.google.gerrit.server.project.GetDescription; import com.google.gerrit.server.project.ListBranches; import com.google.gerrit.server.project.ListChildProjects; +import com.google.gerrit.server.project.ListDashboards; import com.google.gerrit.server.project.ListTags; import com.google.gerrit.server.project.ProjectJson; import com.google.gerrit.server.project.ProjectResource; @@ -68,6 +72,7 @@ import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import java.util.Collections; import java.util.List; public class ProjectApiImpl implements ProjectApi { @@ -104,6 +109,7 @@ private final CommitApiImpl.Factory commitApi; private final DashboardApiImpl.Factory dashboardApi; private final CheckAccess checkAccess; + private final Provider<ListDashboards> listDashboards; @AssistedInject ProjectApiImpl( @@ -132,6 +138,7 @@ CommitApiImpl.Factory commitApi, DashboardApiImpl.Factory dashboardApi, CheckAccess checkAccess, + Provider<ListDashboards> listDashboards, @Assisted ProjectResource project) { this( user, @@ -160,6 +167,7 @@ commitApi, dashboardApi, checkAccess, + listDashboards, null); } @@ -190,6 +198,7 @@ CommitApiImpl.Factory commitApi, DashboardApiImpl.Factory dashboardApi, CheckAccess checkAccess, + Provider<ListDashboards> listDashboards, @Assisted String name) { this( user, @@ -218,6 +227,7 @@ commitApi, dashboardApi, checkAccess, + listDashboards, name); } @@ -248,6 +258,7 @@ CommitApiImpl.Factory commitApi, DashboardApiImpl.Factory dashboardApi, CheckAccess checkAccess, + Provider<ListDashboards> listDashboards, String name) { this.user = user; this.permissionBackend = permissionBackend; @@ -275,6 +286,7 @@ this.createAccessChange = createAccessChange; this.dashboardApi = dashboardApi; this.checkAccess = checkAccess; + this.listDashboards = listDashboards; this.name = name; } @@ -473,6 +485,45 @@ return dashboard(DEFAULT_DASHBOARD_NAME); } + @Override + public void defaultDashboard(String name) throws RestApiException { + try { + dashboardApi.create(checkExists(), name).setDefault(); + } catch (Exception e) { + throw asRestApiException("Cannot set default dashboard", e); + } + } + + @Override + public void removeDefaultDashboard() throws RestApiException { + try { + dashboardApi.create(checkExists(), null).setDefault(); + } catch (Exception e) { + throw asRestApiException("Cannot remove default dashboard", e); + } + } + + @Override + public ListDashboardsRequest dashboards() throws RestApiException { + return new ListDashboardsRequest() { + @Override + public List<DashboardInfo> get() throws RestApiException { + try { + List<?> r = listDashboards.get().apply(checkExists()); + if (r.isEmpty()) { + return Collections.emptyList(); + } + if (r.get(0) instanceof DashboardInfo) { + return r.stream().map(i -> (DashboardInfo) i).collect(toList()); + } + throw new NotImplementedException("list with inheritance"); + } catch (Exception e) { + throw asRestApiException("Cannot list dashboards", e); + } + } + }; + } + private ProjectResource checkExists() throws ResourceNotFoundException { if (project == null) { throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java index ce31cac..988b9df7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -16,7 +16,6 @@ import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountResolver; @@ -24,7 +23,6 @@ import com.google.gerrit.server.config.AuthConfig; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import java.io.IOException; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -36,14 +34,12 @@ import org.kohsuke.args4j.spi.Setter; public class AccountIdHandler extends OptionHandler<Account.Id> { - private final Provider<ReviewDb> db; private final AccountResolver accountResolver; private final AccountManager accountManager; private final AuthType authType; @Inject public AccountIdHandler( - Provider<ReviewDb> db, AccountResolver accountResolver, AccountManager accountManager, AuthConfig authConfig, @@ -51,7 +47,6 @@ @Assisted OptionDef option, @Assisted Setter<Account.Id> setter) { super(parser, option, setter); - this.db = db; this.accountResolver = accountResolver; this.accountManager = accountManager; this.authType = authConfig.getAuthType(); @@ -62,7 +57,7 @@ String token = params.getParameter(0); Account.Id accountId; try { - Account a = accountResolver.find(db.get(), token); + Account a = accountResolver.find(token); if (a != null) { accountId = a.getId(); } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java index a34e3fc..ec803e5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -33,6 +33,7 @@ import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.auth.AuthenticationUnavailableException; +import com.google.gerrit.server.auth.NoSuchUserException; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.GerritServerConfig; import com.google.inject.Inject; @@ -232,7 +233,15 @@ } try { final Helper.LdapSchema schema = helper.getSchema(ctx); - final LdapQuery.Result m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly); + LdapQuery.Result m; + who.setAuthProvidesAccountActiveStatus(true); + try { + m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly); + who.setActive(true); + } catch (NoSuchUserException e) { + who.setActive(false); + return who; + } if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) { // We found the user account, but we need to verify @@ -314,6 +323,19 @@ } } + @Override + public boolean isActive(String username) + throws LoginException, NamingException, AccountException { + try { + DirContext ctx = helper.open(); + Helper.LdapSchema schema = helper.getSchema(ctx); + helper.findAccount(schema, ctx, username, false); + } catch (NoSuchUserException e) { + return false; + } + return true; + } + static class UserLoader extends CacheLoader<String, Optional<Account.Id>> { private final ExternalIds externalIds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java index e2237f3..8dc53bc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -551,22 +551,13 @@ if (user.isIdentifiedUser()) { Collection<String> stars = cd.stars(user.getAccountId()); out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null; - out.muted = - stars.contains(StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId()) - ? true - : null; if (!stars.isEmpty()) { out.stars = stars; } } if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) { - Account.Id accountId = user.getAccountId(); - if (out.muted != null) { - out.reviewed = true; - } else { - out.reviewed = cd.reviewedBy().contains(accountId) ? true : null; - } + out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null; } out.labels = labelsFor(perm, cd, has(LABELS), has(DETAILED_LABELS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java index f980ade..7fffd3a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -31,10 +31,10 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.permissions.RefPermission; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.server.update.RetryingRestModifyView; @@ -54,7 +54,7 @@ private final Provider<CurrentUser> user; private final CherryPickChange cherryPickChange; private final ChangeJson.Factory json; - private final ProjectControl.GenericFactory projectControlFactory; + private final ContributorAgreementsChecker contributorAgreements; @Inject CherryPick( @@ -63,13 +63,13 @@ RetryHelper retryHelper, CherryPickChange cherryPickChange, ChangeJson.Factory json, - ProjectControl.GenericFactory projectControlFactory) { + ContributorAgreementsChecker contributorAgreements) { super(retryHelper); this.permissionBackend = permissionBackend; this.user = user; this.cherryPickChange = cherryPickChange; this.json = json; - this.projectControlFactory = projectControlFactory; + this.contributorAgreements = contributorAgreements; } @Override @@ -85,7 +85,8 @@ } String refName = RefNames.fullName(input.destination); - CreateChange.checkValidCLA(projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser())); + contributorAgreements.check(rsrc.getProject(), rsrc.getUser()); + permissionBackend .user(user) .project(rsrc.getChange().getProject())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java index 0444e0a..4980975 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -30,6 +30,7 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.project.CommitResource; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.update.BatchUpdate; @@ -51,6 +52,7 @@ private final Provider<CurrentUser> user; private final CherryPickChange cherryPickChange; private final ChangeJson.Factory json; + private final ContributorAgreementsChecker contributorAgreements; @Inject CherryPickCommit( @@ -58,12 +60,14 @@ Provider<CurrentUser> user, CherryPickChange cherryPickChange, ChangeJson.Factory json, - PermissionBackend permissionBackend) { + PermissionBackend permissionBackend, + ContributorAgreementsChecker contributorAgreements) { super(retryHelper); this.permissionBackend = permissionBackend; this.user = user; this.cherryPickChange = cherryPickChange; this.json = json; + this.contributorAgreements = contributorAgreements; } @Override @@ -83,7 +87,7 @@ } String refName = RefNames.fullName(destination); - CreateChange.checkValidCLA(rsrc.getProjectState().controlFor(user.get())); + contributorAgreements.check(projectName, user.get()); permissionBackend .user(user) .project(projectName)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java index 3c5ae7d..ba8701c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -20,14 +20,12 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; import com.google.gerrit.extensions.common.MergeInput; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.Response; @@ -57,8 +55,8 @@ import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.project.CommitsCollection; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.InvalidChangeOperationException; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectResource; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.ProjectsCollection; @@ -110,6 +108,7 @@ private final MergeUtil.Factory mergeUtilFactory; private final SubmitType submitType; private final NotifyUtil notifyUtil; + private final ContributorAgreementsChecker contributorAgreements; @Inject CreateChange( @@ -130,7 +129,8 @@ PatchSetUtil psUtil, @GerritServerConfig Config config, MergeUtil.Factory mergeUtilFactory, - NotifyUtil notifyUtil) { + NotifyUtil notifyUtil, + ContributorAgreementsChecker contributorAgreements) { super(retryHelper); this.anonymousCowardName = anonymousCowardName; this.db = db; @@ -149,6 +149,7 @@ this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY); this.mergeUtilFactory = mergeUtilFactory; this.notifyUtil = notifyUtil; + this.contributorAgreements = contributorAgreements; } @Override @@ -175,7 +176,7 @@ } ProjectResource rsrc = projectsCollection.parse(input.project); - checkValidCLA(rsrc.getControl()); + contributorAgreements.check(rsrc.getNameKey(), rsrc.getUser()); Project.NameKey project = rsrc.getNameKey(); String refName = RefNames.fullName(input.branch); @@ -341,11 +342,4 @@ private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException { return inserter.insert(new TreeFormatter()); } - - static void checkValidCLA(ProjectControl ctl) throws AuthException { - Capable capable = ctl.canPushToAtLeastOneRef(); - if (capable != Capable.OK) { - throw new AuthException(capable.getMessage()); - } - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java new file mode 100644 index 0000000..265b2b0 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java
@@ -0,0 +1,78 @@ +// 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. + +package com.google.gerrit.server.change; + +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class MarkAsReviewed + implements RestModifyView<ChangeResource, MarkAsReviewed.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(MarkAsReviewed.class); + + public static class Input {} + + private final Provider<ReviewDb> dbProvider; + private final ChangeData.Factory changeDataFactory; + private final StarredChangesUtil stars; + + @Inject + MarkAsReviewed( + Provider<ReviewDb> dbProvider, + ChangeData.Factory changeDataFactory, + StarredChangesUtil stars) { + this.dbProvider = dbProvider; + this.changeDataFactory = changeDataFactory; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Mark Reviewed") + .setTitle("Mark the change as reviewed to unhighlight it in the dashboard") + .setVisible(!isReviewed(rsrc)); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) + throws RestApiException, OrmException, IllegalLabelException { + stars.markAsReviewed(rsrc); + return Response.ok(""); + } + + private boolean isReviewed(ChangeResource rsrc) { + try { + return changeDataFactory + .create(dbProvider.get(), rsrc.getNotes()) + .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId()); + } catch (OrmException e) { + log.error("failed to check if change is reviewed", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java new file mode 100644 index 0000000..6de84ee --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java
@@ -0,0 +1,77 @@ +// 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. + +package com.google.gerrit.server.change; + +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class MarkAsUnreviewed + implements RestModifyView<ChangeResource, MarkAsUnreviewed.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(MarkAsUnreviewed.class); + + public static class Input {} + + private final Provider<ReviewDb> dbProvider; + private final ChangeData.Factory changeDataFactory; + private final StarredChangesUtil stars; + + @Inject + MarkAsUnreviewed( + Provider<ReviewDb> dbProvider, + ChangeData.Factory changeDataFactory, + StarredChangesUtil stars) { + this.dbProvider = dbProvider; + this.changeDataFactory = changeDataFactory; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Mark Unreviewed") + .setTitle("Mark the change as unreviewed to highlight it in the dashboard") + .setVisible(isReviewed(rsrc)); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) + throws OrmException, IllegalLabelException { + stars.markAsUnreviewed(rsrc); + return Response.ok(""); + } + + private boolean isReviewed(ChangeResource rsrc) { + try { + return changeDataFactory + .create(dbProvider.get(), rsrc.getNotes()) + .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId()); + } catch (OrmException e) { + log.error("failed to check if change is reviewed", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java index f3a6c66..f648d5a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -90,8 +90,8 @@ delete(CHANGE_KIND, "private").to(DeletePrivate.class); put(CHANGE_KIND, "ignore").to(Ignore.class); put(CHANGE_KIND, "unignore").to(Unignore.class); - put(CHANGE_KIND, "mute").to(Mute.class); - put(CHANGE_KIND, "unmute").to(Unmute.class); + put(CHANGE_KIND, "reviewed").to(MarkAsReviewed.class); + put(CHANGE_KIND, "unreviewed").to(MarkAsUnreviewed.class); post(CHANGE_KIND, "wip").to(SetWorkInProgress.class); post(CHANGE_KIND, "ready").to(SetReadyForReview.class); put(CHANGE_KIND, "message").to(PutMessage.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java deleted file mode 100644 index 9da993b..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java +++ /dev/null
@@ -1,80 +0,0 @@ -// 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. - -package com.google.gerrit.server.change; - -import com.google.gerrit.extensions.restapi.BadRequestException; -import com.google.gerrit.extensions.restapi.Response; -import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.extensions.webui.UiAction; -import com.google.gerrit.server.StarredChangesUtil; -import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Singleton -public class Mute implements RestModifyView<ChangeResource, Mute.Input>, UiAction<ChangeResource> { - private static final Logger log = LoggerFactory.getLogger(Mute.class); - - public static class Input {} - - private final StarredChangesUtil stars; - - @Inject - Mute(StarredChangesUtil stars) { - this.stars = stars; - } - - @Override - public Description getDescription(ChangeResource rsrc) { - return new UiAction.Description() - .setLabel("Mute") - .setTitle("Mute the change to unhighlight it in the dashboard") - .setVisible(isMuteable(rsrc)); - } - - @Override - public Response<String> apply(ChangeResource rsrc, Input input) - throws RestApiException, OrmException, IllegalLabelException { - if (rsrc.isUserOwner()) { - throw new BadRequestException("cannot mute own change"); - } - if (!isMuted(rsrc)) { - stars.mute(rsrc); - } - return Response.ok(""); - } - - private boolean isMuted(ChangeResource rsrc) { - try { - return stars.isMuted(rsrc); - } catch (OrmException e) { - log.error("failed to check muted star", e); - } - return false; - } - - private boolean isMuteable(ChangeResource rsrc) { - try { - return !rsrc.isUserOwner() && !isMuted(rsrc) && !stars.isIgnored(rsrc); - } catch (OrmException e) { - log.error("failed to check ignored star", e); - } - return false; - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java index ccc7587..c29faee 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
@@ -25,11 +25,9 @@ import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountResolver; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; @@ -40,12 +38,10 @@ @Singleton public class NotifyUtil { - private final Provider<ReviewDb> dbProvider; private final AccountResolver accountResolver; @Inject - NotifyUtil(Provider<ReviewDb> dbProvider, AccountResolver accountResolver) { - this.dbProvider = dbProvider; + NotifyUtil(AccountResolver accountResolver) { this.accountResolver = accountResolver; } @@ -90,19 +86,19 @@ if (m == null) { m = MultimapBuilder.hashKeys().arrayListValues().build(); } - m.putAll(e.getKey(), find(dbProvider.get(), accounts)); + m.putAll(e.getKey(), find(accounts)); } } return m != null ? m : ImmutableListMultimap.of(); } - private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails) + private List<Account.Id> find(List<String> nameOrEmails) throws OrmException, BadRequestException, IOException, ConfigInvalidException { List<String> missing = new ArrayList<>(nameOrEmails.size()); List<Account.Id> r = new ArrayList<>(nameOrEmails.size()); for (String nameOrEmail : nameOrEmails) { - Account a = accountResolver.find(db, nameOrEmail); + Account a = accountResolver.find(nameOrEmail); if (a != null) { r.add(a.getId()); } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java index 0e57918..c4e2f3b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -26,8 +26,8 @@ import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.edit.ChangeEditUtil; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.server.update.RetryingRestModifyView; @@ -76,18 +76,18 @@ private final ChangeEditUtil editUtil; private final NotifyUtil notifyUtil; - private final ProjectControl.GenericFactory projectControlFactory; + private final ContributorAgreementsChecker contributorAgreementsChecker; @Inject Publish( RetryHelper retryHelper, ChangeEditUtil editUtil, NotifyUtil notifyUtil, - ProjectControl.GenericFactory projectControlFactory) { + ContributorAgreementsChecker contributorAgreementsChecker) { super(retryHelper); this.editUtil = editUtil; this.notifyUtil = notifyUtil; - this.projectControlFactory = projectControlFactory; + this.contributorAgreementsChecker = contributorAgreementsChecker; } @Override @@ -95,8 +95,7 @@ BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in) throws IOException, OrmException, RestApiException, UpdateException, ConfigInvalidException, NoSuchProjectException { - CreateChange.checkValidCLA( - projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser())); + contributorAgreementsChecker.check(rsrc.getProject(), rsrc.getUser()); Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser()); if (!edit.isPresent()) { throw new ResourceConflictException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java index cfb588f..2dfda08 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -46,9 +46,9 @@ import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; @@ -95,7 +95,7 @@ private final PersonIdent serverIdent; private final ApprovalsUtil approvalsUtil; private final ChangeReverted changeReverted; - private final ProjectControl.GenericFactory projectControlFactory; + private final ContributorAgreementsChecker contributorAgreements; @Inject Revert( @@ -112,7 +112,7 @@ @GerritPersonIdent PersonIdent serverIdent, ApprovalsUtil approvalsUtil, ChangeReverted changeReverted, - ProjectControl.GenericFactory projectControlFactory) { + ContributorAgreementsChecker contributorAgreements) { super(retryHelper); this.db = db; this.permissionBackend = permissionBackend; @@ -126,7 +126,7 @@ this.serverIdent = serverIdent; this.approvalsUtil = approvalsUtil; this.changeReverted = changeReverted; - this.projectControlFactory = projectControlFactory; + this.contributorAgreements = contributorAgreements; } @Override @@ -139,7 +139,7 @@ throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } - CreateChange.checkValidCLA(projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser())); + contributorAgreements.check(rsrc.getProject(), rsrc.getUser()); permissionBackend.user(rsrc.getUser()).ref(change.getDest()).check(CREATE_CHANGE); Change.Id revertId =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java deleted file mode 100644 index 16d6d88..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java +++ /dev/null
@@ -1,67 +0,0 @@ -// 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. - -package com.google.gerrit.server.change; - -import com.google.gerrit.extensions.restapi.Response; -import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.extensions.webui.UiAction; -import com.google.gerrit.server.StarredChangesUtil; -import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Singleton -public class Unmute - implements RestModifyView<ChangeResource, Unmute.Input>, UiAction<ChangeResource> { - private static final Logger log = LoggerFactory.getLogger(Unmute.class); - - public static class Input {} - - private final StarredChangesUtil stars; - - @Inject - Unmute(StarredChangesUtil stars) { - this.stars = stars; - } - - @Override - public Description getDescription(ChangeResource rsrc) { - return new UiAction.Description() - .setLabel("Unmute") - .setTitle("Unmute the change") - .setVisible(isMuted(rsrc)); - } - - @Override - public Response<String> apply(ChangeResource rsrc, Input input) - throws OrmException, IllegalLabelException { - if (isMuted(rsrc)) { - stars.unmute(rsrc); - } - return Response.ok(""); - } - - private boolean isMuted(ChangeResource rsrc) { - try { - return stars.isMuted(rsrc); - } catch (OrmException e) { - log.error("failed to check muted star", e); - } - return false; - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 901084e..0e4e8b4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,6 +81,7 @@ import com.google.gerrit.server.Sequences; import com.google.gerrit.server.account.AccountCacheImpl; import com.google.gerrit.server.account.AccountControl; +import com.google.gerrit.server.account.AccountDeactivator; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountVisibilityProvider; @@ -279,6 +280,7 @@ bind(GcConfig.class); bind(ChangeCleanupConfig.class); + bind(AccountDeactivator.class); bind(ApprovalsUtil.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java index 9d7dd19..2dd5f2a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -20,7 +20,6 @@ import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_SELF; import static java.util.stream.Collectors.toMap; -import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; @@ -37,6 +36,7 @@ import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; @@ -118,15 +118,18 @@ refs = addUsersSelfSymref(refs); } - projectCtl = projectState.controlFor(user.get()); - if (projectCtl.allRefsAreVisible(ImmutableSet.of(REFS_CONFIG))) { + PermissionBackend.WithUser withUser = permissionBackend.user(user); + PermissionBackend.ForProject forProject = withUser.project(projectState.getNameKey()); + if (checkProjectPermission(forProject, ProjectPermission.READ)) { + return refs; + } else if (checkProjectPermission(forProject, ProjectPermission.READ_NO_CONFIG)) { return fastHideRefsMetaConfig(refs); } Account.Id userId; boolean viewMetadata; if (user.get().isIdentifiedUser()) { - viewMetadata = permissionBackend.user(user).testOrFalse(GlobalPermission.ACCESS_DATABASE); + viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE); IdentifiedUser u = user.get().asIdentifiedUser(); userId = u.getAccountId(); userEditPrefix = RefNames.refsEditPrefix(userId); @@ -138,6 +141,7 @@ Map<String, Ref> result = new HashMap<>(); List<Ref> deferredTags = new ArrayList<>(); + projectCtl = projectState.controlFor(user.get()); for (Ref ref : refs.values()) { String name = ref.getName(); Change.Id changeId; @@ -336,4 +340,21 @@ return false; } } + + private boolean checkProjectPermission( + PermissionBackend.ForProject forProject, ProjectPermission perm) { + try { + forProject.check(perm); + } catch (AuthException e) { + return false; + } catch (PermissionBackendException e) { + log.error( + String.format( + "Can't check permission for user %s on project %s", + user.get(), projectState.getName()), + e); + return false; + } + return true; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java index 71d8f63..22834f3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -32,6 +32,7 @@ import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.query.change.InternalChangeQuery; @@ -45,6 +46,7 @@ import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.name.Named; +import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; @@ -167,6 +169,7 @@ private final ExecutorService executor; private final RequestScopePropagator scopePropagator; private final ReceiveConfig receiveConfig; + private final ContributorAgreementsChecker contributorAgreements; private final long timeoutMillis; private final ProjectControl projectControl; private final Repository repo; @@ -185,6 +188,7 @@ ReceiveConfig receiveConfig, TransferConfig transferConfig, Provider<LazyPostReceiveHookChain> lazyPostReceive, + ContributorAgreementsChecker contributorAgreements, @Named(TIMEOUT_NAME) long timeoutMillis, @Assisted ProjectControl projectControl, @Assisted Repository repo, @@ -195,6 +199,7 @@ this.executor = executor; this.scopePropagator = scopePropagator; this.receiveConfig = receiveConfig; + this.contributorAgreements = contributorAgreements; this.timeoutMillis = timeoutMillis; this.projectControl = projectControl; this.repo = repo; @@ -235,15 +240,23 @@ } /** Determine if the user can upload commits. */ - public Capable canUpload() { + public Capable canUpload() throws IOException { Capable result = projectControl.canPushToAtLeastOneRef(); if (result != Capable.OK) { return result; } - if (receiveConfig.checkMagicRefs) { - result = MagicBranch.checkMagicBranchRefs(repo, projectControl.getProject()); + + try { + contributorAgreements.check( + projectControl.getProject().getNameKey(), projectControl.getUser()); + } catch (AuthException e) { + return new Capable(e.getMessage()); } - return result; + + if (receiveConfig.checkMagicRefs) { + return MagicBranch.checkMagicBranchRefs(repo, projectControl.getProject()); + } + return Capable.OK; } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java index 3c8bcea..b7aa416 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2126,7 +2126,7 @@ checkNotNull(magicBranch); recipients.add(magicBranch.getMailRecipients()); approvals = magicBranch.labels; - recipients.add(getRecipientsFromFooters(db, accountResolver, footerLines)); + recipients.add(getRecipientsFromFooters(accountResolver, footerLines)); recipients.remove(me); StringBuilder msg = new StringBuilder(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java index bcb8564..4455aed 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -291,7 +291,7 @@ psDescription); update.setPsDescription(psDescription); - recipients.add(getRecipientsFromFooters(ctx.getDb(), accountResolver, commit.getFooterLines())); + recipients.add(getRecipientsFromFooters(accountResolver, commit.getFooterLines())); recipients.remove(ctx.getAccountId()); ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes()); MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java index 96024d2..b6bcb3b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -152,7 +152,7 @@ case HTTP_LDAP: case CLIENT_SSL_CERT_LDAP: case LDAP: - if (accountResolver.find(db.get(), nameOrEmailOrId) == null) { + if (accountResolver.find(nameOrEmailOrId) == null) { // account does not exist, try to create it Account a = createAccountByLdap(nameOrEmailOrId); if (a != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java index c103c89..95bdaab 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -91,7 +91,10 @@ @Deprecated static final Schema<ChangeData> V46 = schema(V45); // Removal of draft change workflow requires reindexing - static final Schema<ChangeData> V47 = schema(V46); + @Deprecated static final Schema<ChangeData> V47 = schema(V46); + + // Rename of star label 'mute' to 'reviewed' requires reindexing + static final Schema<ChangeData> V48 = schema(V47); public static final String NAME = "changes"; public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java index be1c9f5..0487cc0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -20,7 +20,6 @@ import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.account.AccountResolver; import com.google.gwtorm.server.OrmException; @@ -39,15 +38,15 @@ DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ"); public static MailRecipients getRecipientsFromFooters( - ReviewDb db, AccountResolver accountResolver, List<FooterLine> footerLines) + AccountResolver accountResolver, List<FooterLine> footerLines) throws OrmException, IOException { MailRecipients recipients = new MailRecipients(); for (FooterLine footerLine : footerLines) { try { if (isReviewer(footerLine)) { - recipients.reviewers.add(toAccountId(db, accountResolver, footerLine.getValue().trim())); + recipients.reviewers.add(toAccountId(accountResolver, footerLine.getValue().trim())); } else if (footerLine.matches(FooterKey.CC)) { - recipients.cc.add(toAccountId(db, accountResolver, footerLine.getValue().trim())); + recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim())); } } catch (NoSuchAccountException e) { continue; @@ -63,10 +62,9 @@ return recipients; } - private static Account.Id toAccountId( - ReviewDb db, AccountResolver accountResolver, String nameOrEmail) + private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail) throws OrmException, NoSuchAccountException, IOException { - Account a = accountResolver.findByNameOrEmail(db, nameOrEmail); + Account a = accountResolver.findByNameOrEmail(nameOrEmail); if (a == null) { throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java index 3078437..d0abf9a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -35,6 +35,14 @@ READ(Permission.READ), /** + * Can read all non-config references in the repository. + * + * <p>This is the same as {@code READ} but does not check if they user can see refs/meta/config. + * Therefore, callers should check {@code READ} before excluding config refs in a short-circuit. + */ + READ_NO_CONFIG, + + /** * Can create at least one reference in the project. * * <p>This project level permission only validates the user may create some type of reference @@ -62,7 +70,13 @@ * .check(RefPermission.CREATE_CHANGE); * </pre> */ - CREATE_CHANGE; + CREATE_CHANGE, + + /** Can run receive pack. */ + RUN_RECEIVE_PACK, + + /** Can run upload pack. */ + RUN_UPLOAD_PACK; private final String name;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java index 281e37e..b8d3fbc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
@@ -24,7 +24,6 @@ import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.permissions.GlobalPermission; @@ -34,7 +33,6 @@ import com.google.gerrit.server.permissions.RefPermission; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import javax.servlet.http.HttpServletResponse; @@ -43,18 +41,15 @@ @Singleton public class CheckAccess implements RestModifyView<ProjectResource, AccessCheckInput> { private final AccountResolver accountResolver; - private final Provider<ReviewDb> db; private final IdentifiedUser.GenericFactory userFactory; private final PermissionBackend permissionBackend; @Inject CheckAccess( AccountResolver resolver, - Provider<ReviewDb> db, IdentifiedUser.GenericFactory userFactory, PermissionBackend permissionBackend) { this.accountResolver = resolver; - this.db = db; this.userFactory = userFactory; this.permissionBackend = permissionBackend; } @@ -72,7 +67,7 @@ throw new BadRequestException("input requires 'account'"); } - Account match = accountResolver.find(db.get(), input.account); + Account match = accountResolver.find(input.account); if (match == null) { throw new UnprocessableEntityException( String.format("cannot find account %s", input.account));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java new file mode 100644 index 0000000..0033b12 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -0,0 +1,108 @@ +// 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. + +package com.google.gerrit.server.project; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.PageLinks; +import com.google.gerrit.common.data.ContributorAgreement; +import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.common.data.PermissionRule.Action; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.AccountGroup.UUID; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.project.ProjectControl.Metrics; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Singleton +public class ContributorAgreementsChecker { + + private final String canonicalWebUrl; + private final ProjectCache projectCache; + private final Metrics metrics; + + @Inject + ContributorAgreementsChecker( + @CanonicalWebUrl @Nullable String canonicalWebUrl, + ProjectCache projectCache, + Metrics metrics) { + this.canonicalWebUrl = canonicalWebUrl; + this.projectCache = projectCache; + this.metrics = metrics; + } + + /** + * Checks if the user has signed a contributor agreement for the project. + * + * @throws AuthException if the user has not signed a contributor agreement for the project + * @throws IOException if project states could not be loaded + */ + public void check(Project.NameKey project, CurrentUser user) throws IOException, AuthException { + metrics.claCheckCount.increment(); + + ProjectState projectState = projectCache.checkedGet(project); + if (projectState == null) { + throw new IOException("Can't load All-Projects"); + } + + if (!projectState.isUseContributorAgreements()) { + return; + } + + if (!user.isIdentifiedUser()) { + throw new AuthException("Must be logged in to verify Contributor Agreement"); + } + + IdentifiedUser iUser = user.asIdentifiedUser(); + Collection<ContributorAgreement> contributorAgreements = + projectCache.getAllProjects().getConfig().getContributorAgreements(); + List<UUID> okGroupIds = new ArrayList<>(); + for (ContributorAgreement ca : contributorAgreements) { + List<AccountGroup.UUID> groupIds; + groupIds = okGroupIds; + + for (PermissionRule rule : ca.getAccepted()) { + if ((rule.getAction() == Action.ALLOW) + && (rule.getGroup() != null) + && (rule.getGroup().getUUID() != null)) { + groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get())); + } + } + } + + if (!iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) { + final StringBuilder msg = new StringBuilder(); + msg.append("A Contributor Agreement must be completed before uploading"); + if (canonicalWebUrl != null) { + msg.append(":\n\n "); + msg.append(canonicalWebUrl); + msg.append("#"); + msg.append(PageLinks.SETTINGS_AGREEMENTS); + msg.append("\n"); + } else { + msg.append("."); + } + throw new AuthException(msg.toString()); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java index ea2935d..d43a066 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -173,7 +173,7 @@ return views; } - static DashboardInfo newDashboardInfo(String ref, String path) { + public static DashboardInfo newDashboardInfo(String ref, String path) { DashboardInfo info = new DashboardInfo(); info.ref = ref; info.path = path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java index 7296311..958de55 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
@@ -16,12 +16,9 @@ import com.google.gerrit.extensions.api.projects.DashboardInfo; import com.google.gerrit.extensions.common.SetDashboardInput; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.inject.Inject; @@ -40,9 +37,7 @@ @Override public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input) - throws AuthException, BadRequestException, ResourceConflictException, - ResourceNotFoundException, MethodNotAllowedException, IOException, - PermissionBackendException { + throws RestApiException, IOException, PermissionBackendException { if (resource.isProjectDefault()) { SetDashboardInput in = new SetDashboardInput(); in.commitMessage = input != null ? input.commitMessage : null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java index adca214..cdf23bb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -21,9 +21,11 @@ import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.gerrit.extensions.api.projects.DashboardInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -51,11 +53,9 @@ @Override public DashboardInfo apply(DashboardResource resource) - throws ResourceNotFoundException, ResourceConflictException, IOException, - PermissionBackendException { + throws RestApiException, IOException, PermissionBackendException { if (inherited && !resource.isProjectDefault()) { - // inherited flag can only be used with default. - throw new ResourceNotFoundException("inherited"); + throw new BadRequestException("inherited flag can only be used with default"); } String project = resource.getControl().getProject().getName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java index 1d3c58c..6960b47 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -46,7 +46,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class ListDashboards implements RestReadView<ProjectResource> { +public class ListDashboards implements RestReadView<ProjectResource> { private static final Logger log = LoggerFactory.getLogger(ListDashboards.class); private final GitRepositoryManager gitManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java index a443cdb..7a7418c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -16,16 +16,13 @@ import static com.google.common.base.Preconditions.checkArgument; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; -import com.google.gerrit.common.Nullable; -import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.Capable; -import com.google.gerrit.common.data.ContributorAgreement; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRule; -import com.google.gerrit.common.data.PermissionRule.Action; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.metrics.Counter0; import com.google.gerrit.metrics.Description; @@ -34,11 +31,10 @@ import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.GroupMembership; -import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GitReceivePackGroups; import com.google.gerrit.server.config.GitUploadPackGroups; import com.google.gerrit.server.group.SystemGroupBackend; @@ -58,7 +54,6 @@ import com.google.inject.Singleton; import com.google.inject.assistedinject.Assisted; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -129,15 +124,12 @@ private final Set<AccountGroup.UUID> uploadGroups; private final Set<AccountGroup.UUID> receiveGroups; - private final String canonicalWebUrl; private final PermissionBackend.WithUser perm; private final CurrentUser user; private final ProjectState state; private final CommitsCollection commits; private final ChangeControl.Factory changeControlFactory; private final PermissionCollection.Factory permissionFilter; - private final Collection<ContributorAgreement> contributorAgreements; - private final Metrics metrics; private List<SectionMatcher> allSections; private Map<String, RefControl> refControls; @@ -147,23 +139,17 @@ ProjectControl( @GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups, @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups, - ProjectCache pc, PermissionCollection.Factory permissionFilter, CommitsCollection commits, ChangeControl.Factory changeControlFactory, - @CanonicalWebUrl @Nullable String canonicalWebUrl, PermissionBackend permissionBackend, @Assisted CurrentUser who, - @Assisted ProjectState ps, - Metrics metrics) { + @Assisted ProjectState ps) { this.changeControlFactory = changeControlFactory; this.uploadGroups = uploadGroups; this.receiveGroups = receiveGroups; this.permissionFilter = permissionFilter; this.commits = commits; - this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements(); - this.canonicalWebUrl = canonicalWebUrl; - this.metrics = metrics; this.perm = permissionBackend.user(who); user = who; state = ps; @@ -214,31 +200,26 @@ return state.getProject(); } - public boolean allRefsAreVisible(Set<String> ignore) { - // TODO(hiesel) Hide refs/changes and replace this method by a proper READ check of all refs - return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore); - } - /** Is this user a project owner? */ public boolean isOwner() { return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER)) || isAdmin(); } - /** @return {@code Capable.OK} if the user can upload to at least one reference */ + /** + * @return {@code Capable.OK} if the user can upload to at least one reference. Does not check + * Contributor Agreements. + */ public Capable canPushToAtLeastOneRef() { if (!canPerformOnAnyRef(Permission.PUSH) && !canPerformOnAnyRef(Permission.CREATE_TAG) && !isOwner()) { return new Capable("Upload denied for project '" + state.getName() + "'"); } - if (state.isUseContributorAgreements()) { - return verifyActiveContributorAgreement(); - } return Capable.OK; } /** Can the user run upload pack? */ - public boolean canRunUploadPack() { + private boolean canRunUploadPack() { for (AccountGroup.UUID group : uploadGroups) { if (match(group)) { return true; @@ -248,7 +229,7 @@ } /** Can the user run receive pack? */ - public boolean canRunReceivePack() { + private boolean canRunReceivePack() { for (AccountGroup.UUID group : receiveGroups) { if (match(group)) { return true; @@ -257,6 +238,10 @@ return false; } + private boolean allRefsAreVisible(Set<String> ignore) { + return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore); + } + /** Returns whether the project is hidden. */ private boolean isHidden() { return getProject().getState().equals(com.google.gerrit.extensions.client.ProjectState.HIDDEN); @@ -296,46 +281,6 @@ return declaredOwner; } - private Capable verifyActiveContributorAgreement() { - metrics.claCheckCount.increment(); - if (!(user.isIdentifiedUser())) { - return new Capable("Must be logged in to verify Contributor Agreement"); - } - final IdentifiedUser iUser = user.asIdentifiedUser(); - - List<AccountGroup.UUID> okGroupIds = new ArrayList<>(); - for (ContributorAgreement ca : contributorAgreements) { - List<AccountGroup.UUID> groupIds; - groupIds = okGroupIds; - - for (PermissionRule rule : ca.getAccepted()) { - if ((rule.getAction() == Action.ALLOW) - && (rule.getGroup() != null) - && (rule.getGroup().getUUID() != null)) { - groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get())); - } - } - } - - if (iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) { - return Capable.OK; - } - - final StringBuilder msg = new StringBuilder(); - msg.append("A Contributor Agreement must be completed before uploading"); - if (canonicalWebUrl != null) { - msg.append(":\n\n "); - msg.append(canonicalWebUrl); - msg.append("#"); - msg.append(PageLinks.SETTINGS_AGREEMENTS); - msg.append("\n"); - } else { - msg.append("."); - } - msg.append("\n"); - return new Capable(msg.toString()); - } - private boolean canPerformOnAnyRef(String permissionName) { for (SectionMatcher matcher : access()) { AccessSection section = matcher.section; @@ -513,10 +458,18 @@ case READ: return !isHidden() && allRefsAreVisible(Collections.emptySet()); + case READ_NO_CONFIG: + return !isHidden() && allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG)); + case CREATE_REF: return canAddRefs(); case CREATE_CHANGE: return canCreateChanges(); + + case RUN_RECEIVE_PACK: + return canRunReceivePack(); + case RUN_UPLOAD_PACK: + return canRunUploadPack(); } throw new PermissionBackendException(perm + " unsupported"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java index 9222322..21ec077 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -16,12 +16,9 @@ import com.google.gerrit.extensions.api.projects.DashboardInfo; import com.google.gerrit.extensions.common.SetDashboardInput; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.inject.Inject; @@ -30,7 +27,7 @@ import java.io.IOException; @Singleton -class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> { +public class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> { private final Provider<SetDefaultDashboard> defaultSetter; @Inject @@ -40,9 +37,7 @@ @Override public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input) - throws AuthException, BadRequestException, ResourceConflictException, - MethodNotAllowedException, ResourceNotFoundException, IOException, - PermissionBackendException { + throws RestApiException, IOException, PermissionBackendException { if (resource.isProjectDefault()) { return defaultSetter.get().apply(resource, input); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java index 256b6f2..9aa9ae7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.git.MetaDataUpdate; @@ -59,8 +60,7 @@ @Override public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input) - throws AuthException, BadRequestException, ResourceConflictException, - ResourceNotFoundException, IOException, PermissionBackendException { + throws RestApiException, IOException, PermissionBackendException { if (input == null) { input = new SetDashboardInput(); // Delete would set input to null. } @@ -132,8 +132,7 @@ @Override public Response<DashboardInfo> apply(ProjectResource resource, SetDashboardInput input) - throws AuthException, BadRequestException, ResourceConflictException, - ResourceNotFoundException, IOException, PermissionBackendException { + throws RestApiException, IOException, PermissionBackendException { SetDefaultDashboard set = setDefault.get(); set.inherited = inherited; return set.apply(DashboardResource.projectDefault(resource.getControl()), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java index 40b384d..2a71258 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -1084,6 +1084,22 @@ return draftsByUser; } + public boolean isReviewedBy(Account.Id accountId) throws OrmException { + Collection<String> stars = stars(accountId); + + if (stars.contains( + StarredChangesUtil.REVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) { + return true; + } + + if (stars.contains( + StarredChangesUtil.UNREVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) { + return false; + } + + return reviewedBy().contains(accountId); + } + public Set<Account.Id> reviewedBy() throws OrmException { if (reviewedBy == null) { if (!lazyLoad) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index beff5f2..1ae579e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -901,7 +901,7 @@ if (isSelf(who)) { return is_visible(); } - Set<Account.Id> m = args.accountResolver.findAll(args.db.get(), who); + Set<Account.Id> m = args.accountResolver.findAll(who); if (!m.isEmpty()) { List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size()); for (Account.Id id : m) { @@ -1258,7 +1258,7 @@ if (isSelf(who)) { return Collections.singleton(self()); } - Set<Account.Id> matches = args.accountResolver.findAll(args.db.get(), who); + Set<Account.Id> matches = args.accountResolver.findAll(who); if (matches.isEmpty()) { throw error("User " + who + " not found"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java index 6bd6e24..057cc44 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -27,7 +27,6 @@ import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupBackends; @@ -38,7 +37,6 @@ import com.google.gerrit.server.index.group.GroupIndexCollection; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import java.io.IOException; import java.util.List; import java.util.Optional; @@ -58,7 +56,6 @@ new QueryBuilder.Definition<>(GroupQueryBuilder.class); public static class Arguments { - final Provider<ReviewDb> db; final GroupIndex groupIndex; final GroupCache groupCache; final GroupBackend groupBackend; @@ -66,12 +63,10 @@ @Inject Arguments( - Provider<ReviewDb> db, GroupIndexCollection groupIndexCollection, GroupCache groupCache, GroupBackend groupBackend, AccountResolver accountResolver) { - this.db = db; this.groupIndex = groupIndexCollection.getSearchIndex(); this.groupCache = groupCache; this.groupBackend = groupBackend; @@ -189,7 +184,7 @@ private Set<Account.Id> parseAccount(String nameOrEmail) throws QueryParseException, OrmException, IOException, ConfigInvalidException { - Set<Account.Id> foundAccounts = args.accountResolver.findAll(args.db.get(), nameOrEmail); + Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail); if (foundAccounts.isEmpty()) { throw error("User " + nameOrEmail + " not found"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index e92b003..d1cbad6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@ /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - public static final Class<Schema_160> C = Schema_160.class; + public static final Class<Schema_161> C = Schema_161.class; public static int getBinaryVersion() { return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java new file mode 100644 index 0000000..407492d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java
@@ -0,0 +1,76 @@ +// 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. + +package com.google.gerrit.server.schema; + +import static java.util.stream.Collectors.toList; + +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException; +import com.google.gerrit.server.StarredChangesUtil.StarRef; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +public class Schema_161 extends SchemaVersion { + private static final String MUTE_LABEL = "mute"; + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + + @Inject + Schema_161( + Provider<Schema_160> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + try (Repository git = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(git)) { + BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate(); + for (Ref ref : git.getRefDatabase().getRefs(RefNames.REFS_STARRED_CHANGES).values()) { + StarRef starRef = StarredChangesUtil.readLabels(git, ref.getName()); + if (starRef.labels().contains(MUTE_LABEL)) { + ObjectId id = + StarredChangesUtil.writeLabels( + git, + starRef + .labels() + .stream() + .map(l -> l.equals(MUTE_LABEL) ? StarredChangesUtil.REVIEWED_LABEL : l) + .collect(toList())); + bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), id, ref.getName())); + } + } + bru.execute(rw, new TextProgressMonitor()); + } catch (IOException | IllegalLabelException ex) { + throw new OrmException(ex); + } + } +}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy index 50c5fc3..623cfe26 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy
@@ -24,7 +24,7 @@ * @param email * @param fromName */ -{template .Abandoned autoescape="strict" kind="text"} +{template .Abandoned kind="text"} {$fromName} has abandoned this change. {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} {\n}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy index c7d4699..fb8ff78 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
@@ -21,7 +21,7 @@ * @param email * @param fromName */ -{template .AbandonedHtml autoescape="strict" kind="html"} +{template .AbandonedHtml kind="html"} <p> {$fromName} <strong>abandoned</strong> this change. </p>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy index aa2b27d..af99569 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -21,7 +21,7 @@ * adding a new SSH or GPG key to an account. * @param email */ -{template .AddKey autoescape="strict" kind="text"} +{template .AddKey kind="text"} One or more new {$email.keyType} keys have been added to Gerrit Code Review at {sp}{$email.gerritHost}: @@ -68,4 +68,4 @@ This is a send-only email address. Replies to this message will not be read or answered. -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy index 017fd6d..21161ea 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -19,7 +19,7 @@ /** * @param email */ -{template .AddKeyHtml autoescape="strict" kind="html"} +{template .AddKeyHtml kind="html"} <p> One or more new {$email.keyType} keys have been added to Gerrit Code Review at {$email.gerritHost}:
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy index 37ac126..f1d201b 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -21,7 +21,7 @@ * that will be appended to ALL emails related to changes. * @param email */ -{template .ChangeFooter autoescape="strict" kind="text"} +{template .ChangeFooter kind="text"} --{sp} {\n}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy index 00f21db..dea6724 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -20,7 +20,7 @@ * @param change * @param email */ -{template .ChangeFooterHtml autoescape="strict" kind="html"} +{template .ChangeFooterHtml kind="html"} {if $email.changeUrl or $email.settingsUrl} <p> {if $email.changeUrl}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy index 98de6e7..d8cffc4 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy
@@ -23,6 +23,6 @@ * @param change * @param shortProjectName */ -{template .ChangeSubject autoescape="strict" kind="text"} +{template .ChangeSubject kind="text"} Change in {$shortProjectName}[{$branch.shortName}]: {$change.shortSubject} {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy index 7bedc1c..7f3062c 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
@@ -25,7 +25,7 @@ * @param fromName * @param commentFiles */ -{template .Comment autoescape="strict" kind="text"} +{template .Comment kind="text"} {$fromName} has posted comments on this change. {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} {\n}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy index 73fdfba..3998438 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
@@ -21,5 +21,5 @@ * that will be appended to emails related to a user submitting comments on * changes. */ -{template .CommentFooter autoescape="strict" kind="text"} +{template .CommentFooter kind="text"} {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy index 7bf28e7..c54f926 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy
@@ -16,5 +16,5 @@ {namespace com.google.gerrit.server.mail.template} -{template .CommentFooterHtml autoescape="strict" kind="html"} +{template .CommentFooterHtml kind="html"} {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy index 870ad46..9b96d69 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -24,7 +24,7 @@ * @param patchSet * @param patchSetCommentBlocks */ -{template .CommentHtml autoescape="strict" kind="html"} +{template .CommentHtml kind="html"} {let $commentHeaderStyle kind="css"} margin-bottom: 4px; {/let}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy index 888ee4b..fc1d60f 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -24,7 +24,7 @@ * @param email * @param fromName */ -{template .DeleteReviewer autoescape="strict" kind="text"} +{template .DeleteReviewer kind="text"} {$fromName} has removed{sp} {foreach $reviewerName in $email.reviewerNames} {if not isFirst($reviewerName)},{sp}{/if} @@ -41,4 +41,4 @@ {$coverLetter} {\n} {/if} -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy index 5faa411..74e5ee5 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -20,7 +20,7 @@ * @param email * @param fromName */ -{template .DeleteReviewerHtml autoescape="strict" kind="html"} +{template .DeleteReviewerHtml kind="html"} <p> {$fromName}{sp} <strong>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy index b249ded..724e90d 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy
@@ -23,7 +23,7 @@ * @param coverLetter * @param fromName */ -{template .DeleteVote autoescape="strict" kind="text"} +{template .DeleteVote kind="text"} {$fromName} has removed a vote on this change.{\n} {\n} Change subject: {$change.subject}{\n} @@ -34,4 +34,4 @@ {$coverLetter} {\n} {/if} -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy index 3d76ae2..06f5456 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
@@ -21,7 +21,7 @@ * @param email * @param fromName */ -{template .DeleteVoteHtml autoescape="strict" kind="html"} +{template .DeleteVoteHtml kind="html"} <p> {$fromName} <strong>removed a vote</strong> from this change. </p>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy index 24db2fd..2b146ec 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
@@ -22,7 +22,7 @@ * CommentFooter. * @param footers */ -{template .Footer autoescape="strict" kind="text"} +{template .Footer kind="text"} {foreach $footer in $footers} {$footer}{\n} {/foreach}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy index 9f9c503..d9f13ce 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
@@ -19,7 +19,7 @@ /** * @param footers */ -{template .FooterHtml autoescape="strict" kind="html"} +{template .FooterHtml kind="html"} {\n} {\n} {foreach $footer in $footers}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HeaderHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HeaderHtml.soy index fdc3fee..85b56ec 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HeaderHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HeaderHtml.soy
@@ -16,5 +16,5 @@ {namespace com.google.gerrit.server.mail.template} -{template .HeaderHtml autoescape="strict" kind="html"} +{template .HeaderHtml kind="html"} {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy index d483264..40924e6 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy
@@ -24,7 +24,7 @@ * @param email * @param fromName */ -{template .Merged autoescape="strict" kind="text"} +{template .Merged kind="text"} {$fromName} has submitted this change and it was merged. {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} {\n} @@ -39,4 +39,4 @@ {$email.unifiedDiff} {\n} {/if} -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy index 927601b..08d37cc 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -21,7 +21,7 @@ * @param email * @param fromName */ -{template .MergedHtml autoescape="strict" kind="html"} +{template .MergedHtml kind="html"} <p> {$fromName} <strong>merged</strong> this change. </p>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy index 9f7429f..ca24d19 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -25,7 +25,7 @@ * @param patchSet * @param projectName */ -{template .NewChange autoescape="strict" kind="text"} +{template .NewChange kind="text"} {if $email.reviewerNames} Hello{sp} {foreach $reviewerName in $email.reviewerNames}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy index 8026666..676f019 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -24,7 +24,7 @@ * @param patchSet * @param projectName */ -{template .NewChangeHtml autoescape="strict" kind="html"} +{template .NewChangeHtml kind="html"} <p> {if $email.reviewerNames} {$fromName} would like{sp}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy index b26535b..c1ac5b6 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy
@@ -24,7 +24,7 @@ * Private template to generate "View Change" buttons. * @param email */ -{template .ViewChangeButton autoescape="strict" kind="html"} +{template .ViewChangeButton kind="html"} <a href="{$email.changeUrl}">View Change</a> {/template} @@ -32,7 +32,7 @@ * Private template to render PRE block with consistent font-sizing. * @param content */ -{template .Pre autoescape="strict" kind="html"} +{template .Pre kind="html"} {let $preStyle kind="css"} font-family: monospace,monospace; // Use this to avoid browsers scaling down // monospace text. @@ -56,7 +56,7 @@ * * @param content */ -{template .WikiFormat autoescape="strict" kind="html"} +{template .WikiFormat kind="html"} {let $blockquoteStyle kind="css"} border-left: 1px solid #aaa; margin: 10px 0; @@ -90,7 +90,7 @@ /** * @param diffLines */ -{template .UnifiedDiff autoescape="strict" kind="html"} +{template .UnifiedDiff kind="html"} {let $addStyle kind="css"} color: hsl(120, 100%, 40%); {/let}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy index 2b30ae6..2886cc0 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
@@ -21,7 +21,7 @@ * related to registering new email accounts. * @param email */ -{template .RegisterNewEmail autoescape="strict" kind="text"} +{template .RegisterNewEmail kind="text"} Welcome to Gerrit Code Review at {$email.gerritHost}.{\n} {\n}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy index e41bdda..124cdf3 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
@@ -26,7 +26,7 @@ * @param patchSet * @param projectName */ -{template .ReplacePatchSet autoescape="strict" kind="text"} +{template .ReplacePatchSet kind="text"} {if $email.reviewerNames and $fromEmail == $change.ownerEmail} Hello{sp} {foreach $reviewerName in $email.reviewerNames} @@ -60,4 +60,4 @@ {$patchSet.refName} {\n} {/if} -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy index 05c60a1..221a4e6 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
@@ -24,7 +24,7 @@ * @param patchSet * @param projectName */ -{template .ReplacePatchSetHtml autoescape="strict" kind="html"} +{template .ReplacePatchSetHtml kind="html"} <p> {$fromName} <strong>uploaded patch set #{$patchSet.patchSetId}</strong>{sp} to{sp}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy index 14ae0f3..4fc6d8c 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy
@@ -24,7 +24,7 @@ * @param email * @param fromName */ -{template .Restored autoescape="strict" kind="text"} +{template .Restored kind="text"} {$fromName} has restored this change. {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} {\n} @@ -36,4 +36,4 @@ {$coverLetter} {\n} {/if} -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy index ea4f615..fdc68b0 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy
@@ -20,7 +20,7 @@ * @param email * @param fromName */ -{template .RestoredHtml autoescape="strict" kind="html"} +{template .RestoredHtml kind="html"} <p> {$fromName} <strong>restored</strong> this change. </p>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy index 7f74df9..09e32ff 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy
@@ -24,7 +24,7 @@ * @param email * @param fromName */ -{template .Reverted autoescape="strict" kind="text"} +{template .Reverted kind="text"} {$fromName} has reverted this change. {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} {\n}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy index d6407e7..479eae1 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy
@@ -20,7 +20,7 @@ * @param email * @param fromName */ -{template .RevertedHtml autoescape="strict" kind="html"} +{template .RevertedHtml kind="html"} <p> {$fromName} <strong>reverted</strong> this change. </p>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.soy index ca4f267..98290e9 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.soy
@@ -25,7 +25,7 @@ * @param patchSet * @param projectName */ -{template .SetAssignee autoescape="strict" kind="text"} +{template .SetAssignee kind="text"} Hello{sp} {$email.assigneeName},
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy index 31cfbd6..d057ba3 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
@@ -23,7 +23,7 @@ * @param patchSet * @param projectName */ -{template .SetAssigneeHtml autoescape="strict" kind="html"} +{template .SetAssigneeHtml kind="html"} <p> {$fromName} has <strong>assigned</strong> a change to{sp} {$email.assigneeName}.{sp}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java index b7a3788..f7ce73f 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -207,7 +207,6 @@ @Inject private SingleVersionListener singleVersionListener; @Inject private InMemoryDatabase schemaFactory; @Inject private ThreadLocalRequestContext requestContext; - @Inject private ProjectControl.Metrics metrics; @Before public void setUp() throws Exception { @@ -871,15 +870,12 @@ return new ProjectControl( Collections.<AccountGroup.UUID>emptySet(), Collections.<AccountGroup.UUID>emptySet(), - projectCache, sectionSorter, null, // commitsCollection changeControlFactory, - "http://localhost", // canonicalWebUrl permissionBackend, new MockUser(name, memberOf), - newProjectState(local), - metrics); + newProjectState(local)); } private ProjectState newProjectState(ProjectConfig local) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java index bbe736d..275da7c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -22,7 +22,6 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.VisibleRefFilter; @@ -51,7 +50,6 @@ @Inject private AccountResolver accountResolver; @Inject private OneOffRequestContext requestContext; @Inject private VisibleRefFilter.Factory refFilterFactory; - @Inject private ReviewDb db; @Inject private GitRepositoryManager repoManager; @Option( @@ -79,7 +77,7 @@ protected void run() throws Failure { Account userAccount; try { - userAccount = accountResolver.find(db, userName); + userAccount = accountResolver.find(userName); } catch (OrmException | IOException | ConfigInvalidException e) { throw die(e); }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java index aa8c562..0f68d61 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -17,11 +17,15 @@ import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.gerrit.common.data.Capable; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.receive.AsyncReceiveCommits; import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.sshd.AbstractGitCommand; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshSession; @@ -51,6 +55,7 @@ @Inject private AsyncReceiveCommits.Factory factory; @Inject private IdentifiedUser currentUser; @Inject private SshSession session; + @Inject private PermissionBackend permissionBackend; private final SetMultimap<ReviewerStateInternal, Account.Id> reviewers = MultimapBuilder.hashKeys(2).hashSetValues().build(); @@ -77,8 +82,15 @@ @Override protected void runImpl() throws IOException, Failure { - if (!projectControl.canRunReceivePack()) { + try { + permissionBackend + .user(currentUser) + .project(project.getNameKey()) + .check(ProjectPermission.RUN_RECEIVE_PACK); + } catch (AuthException e) { throw new Failure(1, "fatal: receive-pack not permitted on this server"); + } catch (PermissionBackendException e) { + throw new Failure(1, "fatal: unable to check permissions " + e); } AsyncReceiveCommits arc = factory.create(projectControl, repo, null, reviewers);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java index fc3a917..2d44b59 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -16,10 +16,14 @@ import com.google.common.collect.Lists; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.validators.UploadValidationException; import com.google.gerrit.server.git.validators.UploadValidators; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.sshd.AbstractGitCommand; import com.google.gerrit.sshd.SshSession; import com.google.inject.Inject; @@ -39,11 +43,19 @@ @Inject private DynamicSet<PostUploadHook> postUploadHooks; @Inject private UploadValidators.Factory uploadValidatorsFactory; @Inject private SshSession session; + @Inject private PermissionBackend permissionBackend; @Override protected void runImpl() throws IOException, Failure { - if (!projectControl.canRunUploadPack()) { + try { + permissionBackend + .user(projectControl.getUser()) + .project(projectControl.getProject().getNameKey()) + .check(ProjectPermission.RUN_UPLOAD_PACK); + } catch (AuthException e) { throw new Failure(1, "fatal: upload-pack not permitted on this server"); + } catch (PermissionBackendException e) { + throw new Failure(1, "fatal: unable to check permissions " + e); } final UploadPack up = new UploadPack(repo);
diff --git a/fake_pom.xml b/gerrit-war/pom.xml similarity index 84% copy from fake_pom.xml copy to gerrit-war/pom.xml index 6ec45e5..c43c098 100644 --- a/fake_pom.xml +++ b/gerrit-war/pom.xml
@@ -1,11 +1,11 @@ <project> <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> - <artifactId>gerrit</artifactId> - <version>1</version> <!-- Do not edit; see version.bzl. --> - <packaging>jar</packaging> - <name>Gerrit Code Review - Extension API</name> - <description>API for Gerrit Extensions</description> + <artifactId>gerrit-war</artifactId> + <version>2.16-SNAPSHOT</version> + <packaging>war</packaging> + <name>Gerrit Code Review - WAR</name> + <description>Gerrit WAR</description> <url>https://www.gerritcodereview.com/</url> <licenses> @@ -53,15 +53,9 @@ <name>Logan Hanks</name> </developer> <developer> - <name>Luca Milanesio</name> - </developer> - <developer> <name>Martin Fick</name> </developer> <developer> - <name>Patrick Hiesel</name> - </developer> - <developer> <name>Saša Živkov</name> </developer> <developer>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 2d4c1d1..9a02fcd 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -33,6 +33,7 @@ import com.google.gerrit.pgm.util.LogFileCompressor; import com.google.gerrit.server.LibModuleLoader; import com.google.gerrit.server.StartupChecks; +import com.google.gerrit.server.account.AccountDeactivator; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -365,6 +366,7 @@ }); modules.add(new GarbageCollectionModule()); modules.add(new ChangeCleanupRunner.Module()); + modules.add(new AccountDeactivator.Module()); modules.addAll(LibModuleLoader.loadModules(cfgInjector)); return cfgInjector.createChildInjector(modules); }
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html new file mode 100644 index 0000000..0148377 --- /dev/null +++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
@@ -0,0 +1,37 @@ +<!-- +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. +--> + +<script> +(function(window) { + 'use strict'; + + window.Gerrit = window.Gerrit || {}; + + /** @polymerBehavior Gerrit.AsyncForeachBehavior */ + Gerrit.AsyncForeachBehavior = { + /** + * @template T + * @param {!Array<T>} array + * @param {!Function} fn + * @return {!Promise<undefined>} + */ + asyncForeach(array, fn) { + if (!array.length) { return Promise.resolve(); } + return fn(array[0]).then(() => this.asyncForeach(array.slice(1), fn)); + }, + }; +})(window); +</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html new file mode 100644 index 0000000..ba15ad7 --- /dev/null +++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -0,0 +1,39 @@ +<!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>async-foreach-behavior</title> + +<script src="../../bower_components/webcomponentsjs/webcomponents.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="async-foreach-behavior.html"> + +<script> + suite('async-foreach-behavior tests', () => { + test('loops over each item', () => { + const fn = sinon.stub().returns(Promise.resolve()); + return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn) + .then(() => { + assert.isTrue(fn.calledThrice); + assert.equal(fn.getCall(0).args[0], 1); + assert.equal(fn.getCall(1).args[0], 2); + assert.equal(fn.getCall(2).args[0], 3); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html index 9936730..13c232e 100644 --- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -156,7 +156,6 @@ }; }); } - patchNums.sort((a, b) => { return a.num - b.num; }); return Gerrit.PatchSetBehavior._computeWipForPatchSets(change, patchNums); },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html index b86d0f7..d79dc69 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -20,6 +20,7 @@ <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="../gr-change-list/gr-change-list.html"> +<link rel="import" href="../gr-user-header/gr-user-header.html"> <link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-list-view"> @@ -46,6 +47,9 @@ nav a:first-of-type { margin-right: .5em; } + .hide { + display: none; + } @media only screen and (max-width: 50em) { .loading, .error { @@ -55,6 +59,9 @@ </style> <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div> <div hidden$="[[_loading]]" hidden> + <gr-user-header + user-id="[[_userId]]" + class$="[[_computeUserHeaderClass(_userId)]]"></gr-user-header> <gr-change-list changes="{{_changes}}" selected-index="{{viewState.selectedChangeIndex}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js index 30fc679..cc35ff8 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -19,6 +19,8 @@ CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g, }; + const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/; + const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i; Polymer({ @@ -84,7 +86,10 @@ /** * Change objects loaded from the server. */ - _changes: Array, + _changes: { + type: Array, + observer: '_changesChanged', + }, /** * For showing a "loading..." string during ajax requests. @@ -93,6 +98,12 @@ type: Boolean, value: true, }, + + /** @type {?String} */ + _userId: { + type: String, + value: null, + }, }, listeners: { @@ -188,5 +199,18 @@ page.show(this._computeNavLink( this._query, this._offset, -1, this._changesPerPage)); }, + + _changesChanged(changes) { + if (!changes || !changes.length || + !USER_QUERY_PATTERN.test(this._query)) { + this._userId = null; + return; + } + this._userId = changes[0].owner.email; + }, + + _computeUserHeaderClass(userId) { + return userId ? '' : 'hide'; + }, }); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html index d70f0c9..5a28565 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -167,6 +167,21 @@ assert.isTrue(showStub.called); }); + test('_userId query', done => { + assert.isNull(element._userId); + element._query = 'owner: foo@bar'; + element._changes = [{owner: {email: 'foo@bar'}}]; + flush(() => { + assert.equal(element._userId, 'foo@bar'); + + element._query = 'foo bar baz'; + element._changes = [{owner: {email: 'foo@bar'}}]; + assert.isNull(element._userId); + + done(); + }); + }); + suite('query based navigation', () => { test('Searching for a change ID redirects to change', done => { sandbox.stub(element, '_getChanges')
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html new file mode 100644 index 0000000..9a7ca33 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -0,0 +1,88 @@ +<!-- +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="../../shared/gr-avatar/gr-avatar.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.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-user-header"> + <template> + <style include="shared-styles"> + :host { + display: block; + height: 9em; + position: relative; + width: 100%; + } + gr-avatar { + height: 7em; + left: 1em; + position: absolute; + top: 1em; + width: 7em; + } + .info { + left: 9em; + position: absolute; + top: 1em; + } + .info > div > span { + display: inline-block; + font-weight: bold; + text-align: right; + width: 6em; + } + .name { + margin-bottom: .25em; + } + .name hr { + width: 100%; + } + .status.hide, + .name.hide { + display: none; + } + </style> + <gr-avatar + account="[[_accountDetails]]" + image-size="100" + aria-label="Account avatar"></gr-avatar> + <div class="info"> + <h1 class$="name"> + [[_computeDetail(_accountDetails, 'name')]] + <hr/> + </h1> + <div class$="status [[_computeStatusClass(_accountDetails)]]"> + <span>Status:</span> [[_status]] + </div> + <div> + <span>Email:</span> + <a href="mailto:[[_computeDetail(_accountDetails, 'email')]]"><!-- + -->[[_computeDetail(_accountDetails, 'email')]]</a> + </div> + <div> + <span>Joined:</span> + <gr-date-formatter + date-str="[[_computeDetail(_accountDetails, 'registered_on')]]"> + </gr-date-formatter> + </div> + </div> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-user-header.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js new file mode 100644 index 0000000..dd3512a --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -0,0 +1,68 @@ +// 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-user-header', + properties: { + /** @type {?String} */ + userId: { + type: String, + observer: '_accountChanged', + }, + + /** + * @type {?{name: ?, email: ?, registered_on: ?}} + */ + _accountDetails: { + type: Object, + value: null, + }, + + /** @type {?String} */ + _status: { + type: String, + value: null, + }, + }, + + _accountChanged(userId) { + if (!userId) { + this._accountDetails = null; + this._status = null; + return; + } + + this.$.restAPI.getAccountDetails(userId).then(details => { + this._accountDetails = details; + }); + this.$.restAPI.getAccountStatus(userId).then(status => { + this._status = status; + }); + }, + + _computeDisplayClass(status) { + return status ? ' ' : 'hide'; + }, + + _computeDetail(accountDetails, name) { + return accountDetails ? accountDetails[name] : ''; + }, + + _computeStatusClass(accountDetails) { + return this._computeDetail(accountDetails, 'status') ? '' : 'hide'; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html new file mode 100644 index 0000000..ab3b249 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -0,0 +1,72 @@ +<!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-user-header</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-user-header.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-user-header></gr-user-header> + </template> +</test-fixture> + +<script> + suite('gr-user-header tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + + test('loads and clears account info', done => { + sandbox.stub(element.$.restAPI, 'getAccountDetails') + .returns(Promise.resolve({ + name: 'foo', + email: 'bar', + registered_on: '2015-03-12 18:32:08.000000000', + })); + sandbox.stub(element.$.restAPI, 'getAccountStatus') + .returns(Promise.resolve('baz')); + + element.userId = 'foo.bar@baz'; + flush(() => { + assert.isOk(element._accountDetails); + assert.isOk(element._status); + + element.userId = null; + flush(() => { + flushAsynchronousOperations(); + assert.isNull(element._accountDetails); + assert.isNull(element._status); + + done(); + }); + }); + }); + }); +</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 f69e393..8f2468b 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
@@ -54,15 +54,15 @@ DELETE_EDIT: 'deleteEdit', IGNORE: 'ignore', MOVE: 'move', - MUTE: 'mute', PRIVATE: 'private', PRIVATE_DELETE: 'private.delete', PUBLISH_EDIT: 'publishEdit', REBASE_EDIT: 'rebaseEdit', RESTORE: 'restore', REVERT: 'revert', + REVIEWED: 'reviewed', UNIGNORE: 'unignore', - UNMUTE: 'unmute', + UNREVIEWED: 'unreviewed', WIP: 'wip', }; @@ -267,11 +267,11 @@ }, { type: ActionType.CHANGE, - key: ChangeActions.MUTE, + key: ChangeActions.REVIEWED, }, { type: ActionType.CHANGE, - key: ChangeActions.UNMUTE, + key: ChangeActions.UNREVIEWED, }, { type: ActionType.CHANGE, @@ -666,7 +666,7 @@ _handleActionTap(e) { e.preventDefault(); - const el = Polymer.dom(e).rootTarget; + const el = e.currentTarget; const key = el.getAttribute('data-action-key'); if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX)) { this.fire(`${key}-tap`, {node: el});
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 e099170..70d26bf 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
@@ -866,22 +866,22 @@ }); }); - suite('mute change', () => { + suite('reviewed change', () => { setup(done => { sandbox.stub(element, '_fireAction'); - const MuteAction = { - __key: 'mute', + const ReviewedAction = { + __key: 'reviewed', __type: 'change', __primary: false, method: 'PUT', - label: 'Mute', + label: 'Mark reviewed', title: 'Working...', enabled: true, }; element.actions = { - mute: MuteAction, + reviewed: ReviewedAction, }; element.changeNum = '2'; @@ -890,37 +890,38 @@ element.reload().then(() => { flush(done); }); }); - test('make sure the mute button is not outside of the overflow menu', + test('make sure the reviewed button is not outside of the overflow menu', () => { - assert.isNotOk(element.$$('[data-action-key="mute"]')); + assert.isNotOk(element.$$('[data-action-key="reviewed"]')); }); - test('muting change', () => { - assert.isOk(element.$.moreActions.$$('span[data-id="mute-change"]')); - element.setActionOverflow('change', 'mute', false); + test('reviewing change', () => { + assert.isOk( + element.$.moreActions.$$('span[data-id="reviewed-change"]')); + element.setActionOverflow('change', 'reviewed', false); flushAsynchronousOperations(); - assert.isOk(element.$$('[data-action-key="mute"]')); + assert.isOk(element.$$('[data-action-key="reviewed"]')); assert.isNotOk( - element.$.moreActions.$$('span[data-id="mute-change"]')); + element.$.moreActions.$$('span[data-id="reviewed-change"]')); }); }); - suite('unmute change', () => { + suite('unreviewed change', () => { setup(done => { sandbox.stub(element, '_fireAction'); - const UnmuteAction = { - __key: 'unmute', + const UnreviewedAction = { + __key: 'unreviewed', __type: 'change', __primary: false, method: 'PUT', - label: 'Unmute', + label: 'Mark unreviewed', title: 'Working...', enabled: true, }; element.actions = { - unmute: UnmuteAction, + unreviewed: UnreviewedAction, }; element.changeNum = '2'; @@ -930,18 +931,18 @@ }); - test('unmute button not outside of the overflow menu', () => { - assert.isNotOk(element.$$('[data-action-key="unmute"]')); + test('unreviewed button not outside of the overflow menu', () => { + assert.isNotOk(element.$$('[data-action-key="unreviewed"]')); }); - test('unmuting change', () => { + test('unreviewed change', () => { assert.isOk( - element.$.moreActions.$$('span[data-id="unmute-change"]')); - element.setActionOverflow('change', 'unmute', false); + element.$.moreActions.$$('span[data-id="unreviewed-change"]')); + element.setActionOverflow('change', 'unreviewed', false); flushAsynchronousOperations(); - assert.isOk(element.$$('[data-action-key="unmute"]')); + assert.isOk(element.$$('[data-action-key="unreviewed"]')); assert.isNotOk( - element.$.moreActions.$$('span[data-id="unmute-change"]')); + element.$.moreActions.$$('span[data-id="unreviewed-change"]')); }); });
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html index 6129e9b..90776c0 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -93,7 +93,8 @@ plugin: { js_resource_paths: [], html_resource_paths: [ - new URL('test/plugin.html', window.location.href).toString(), + new URL('test/plugin.html?' + Math.random(), + window.location.href).toString(), ], }, };
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html index cbaf605..559157d 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -107,6 +107,9 @@ opacity: 0; pointer-events: none; } + .hashtagChip { + margin-bottom: .5em; + } #externalStyle { display: block; } @@ -222,6 +225,7 @@ </template> <template is="dom-if" if="[[!change.topic]]"> <gr-editable-label + uppercase label-text="Add a topic" value="[[change.topic]]" placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" @@ -240,6 +244,7 @@ <span class="value"> <template is="dom-repeat" items="[[change.hashtags]]"> <gr-linked-chip + class="hashtagChip" text="[[item]]" href="[[_computeHashtagURL(item)]]" removable="[[!_hashtagReadOnly]]" @@ -247,6 +252,7 @@ </gr-linked-chip> </template> <gr-editable-label + uppercase label-text="Add a hashtag" value="{{_newHashtag}}" placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js index b5b909a..1fb1c15 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - const HASHTAG_ADD_MESSAGE = 'Click to add'; + const HASHTAG_ADD_MESSAGE = 'Add Hashtag'; const SubmitTypeLabel = { FAST_FORWARD_ONLY: 'Fast Forward Only', @@ -209,7 +209,7 @@ }, _computeTopicPlaceholder(_topicReadOnly) { - return _topicReadOnly ? 'No Topic' : 'Click to add topic'; + return _topicReadOnly ? 'No Topic' : 'Add Topic'; }, _computeHashtagPlaceholder(_hashtagReadOnly) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html index 031acda..e4eea51 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -200,13 +200,6 @@ .showOnEdit { display: none; } - .patchInfo { - border: 1px solid #ddd; - margin: 1em var(--default-horizontal-margin); - } - #fileList { - padding: .5em calc(var(--default-horizontal-margin) / 2); - } .scrollable { overflow: auto; } @@ -226,6 +219,9 @@ min-width: 0; } } + .patchInfo { + margin-top: 1em; + } /* NOTE: If you update this breakpoint, also update the BREAKPOINT_RELATED_SMALL in the JS */ @media screen and (max-width: 50em) { @@ -438,7 +434,8 @@ shown-file-count="[[_shownFileCount]]" diff-prefs="[[_diffPrefs]]" diff-view-mode="{{viewState.diffMode}}" - patch-range="{{_patchRange}}" + patch-num="{{_patchNum}}" + base-patch-num="{{_basePatchNum}}" revisions="[[_sortedRevisions]]" on-open-diff-prefs="_handleOpenDiffPrefs" on-open-download-dialog="_handleOpenDownloadDialog"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js index 31faa54..fd20e44 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -137,6 +137,16 @@ _patchRange: { type: Object, }, + // These are kept as separate properties from the patchRange so that the + // observer can be aware of the previous value. In order to view sub + // property changes for _patchRange, a complex observer must be used, and + // that only displays the new value. + // + // If a previous value did not exist, the change is not reloaded with the + // new patches. This is just the initial setting from the change view vs. + // an update coming from the two way data binding. + _patchNum: String, + _basePatchNum: String, _relatedChangesLoading: { type: Boolean, value: true, @@ -187,7 +197,7 @@ _sortedRevisions: Array, _editLoaded: { type: Boolean, - computed: '_computeEditLoaded(_patchRange.*)', + computed: '_computeEditLoaded(_patchNum)', }, }, @@ -211,6 +221,7 @@ '_labelsChanged(_change.labels.*)', '_paramsAndChangeChanged(params, _change)', '_updateSortedRevisions(_change.revisions.*)', + '_patchRangeChanged(_patchRange.*)', ], keyBindings: { @@ -311,6 +322,15 @@ window.location.reload(); }, + /** + * Called when the patch range changes. does not detect sub property + * updates. + */ + _patchRangeChanged() { + this._basePatchNum = this._patchRange.basePatchNum; + this._patchNum = this._patchRange.patchNum; + }, + _handleCommitMessageCancel(e) { this._editingCommitMessage = false; }, @@ -501,8 +521,8 @@ if (this._initialLoadComplete && patchChanged) { if (patchRange.patchNum == null) { patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets); + this._patchRange = patchRange; } - this._patchRange = patchRange; this._reloadPatchNumDependentResources().then(() => { this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { change: this._change, @@ -1267,9 +1287,8 @@ return change.work_in_progress ? 'header wip' : 'header'; }, - _computeEditLoaded(patchRangeRecord) { - const patchRange = patchRangeRecord.base || {}; - return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); + _computeEditLoaded(patchNum) { + return this.patchNumEquals(patchNum, this.EDIT_NAME); }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html index c1b1a90..27e2262 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -778,6 +778,26 @@ assert.isNull(element._getUrlParameter('test')); }); + test('navigateToChange called when range select changes', () => { + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2}, + rev1: {_number: 1}, + rev13: {_number: 13}, + rev3: {_number: 3}, + }, + status: 'NEW', + labels: {}, + }; + element._basePatchNum = 1; + element._patchNum = 2; + element._patchNum = 3; + assert.equal(navigateToChangeStub.callCount, 1); + assert.isTrue(navigateToChangeStub.lastCall + .calledWithExactly(element._change, 3, 1)); + }); + test('revert dialog opened with revert param', done => { sandbox.stub(element.$.restAPI, 'getLoggedIn', () => { return Promise.resolve(true); @@ -794,6 +814,7 @@ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', revisions: { rev1: {_number: 1}, + rev2: {_number: 2}, }, current_revision: 'rev1', status: element.ChangeStatus.MERGED, @@ -1128,11 +1149,9 @@ }); test('_computeEditLoaded', () => { - const callCompute = range => element._computeEditLoaded({base: range}); - assert.isFalse(callCompute({})); - assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1})); - assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1})); - assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'})); + assert.isFalse(element._computeEditLoaded(1)); + assert.isFalse(element._computeEditLoaded('')); + assert.isTrue(element._computeEditLoaded(element.EDIT_NAME)); }); test('_processEdit', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html index 252ea71..57b3c10 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -40,23 +40,33 @@ .patchInfoOldPatchSet.patchInfo-header { background-color: #fff9c4; } - .patchInfoOldPatchSet .latestPatchContainer { - display: initial; - } .patchInfo-header { - background-color: #f6f6f6; - border-bottom: 1px solid #ebebeb; + background-color: #fafafa; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; display: flex; - padding: .5em calc(var(--default-horizontal-margin) / 2); + min-height: 3.2em; + padding: .5em var(--default-horizontal-margin); } .patchInfo-header-wrapper { align-items: center; display: flex; width: 100%; } - .latestPatchContainer { + .patchInfo-left { + align-items: center; + display: flex; + flex-wrap: wrap; + } + .patchInfo-header-wrapper .container.latestPatchContainer { display: none; } + .patchInfoOldPatchSet .container.latestPatchContainer { + display: initial; + } + .latestPatchContainer a { + text-decoration: none; + } gr-editable-label.descriptionLabel { max-width: 100%; } @@ -77,11 +87,20 @@ .editLoaded .showOnEdit { display: initial; } + .patchInfo-header-wrapper .container { + align-items: center; + display: flex; + } + #modeSelect { + margin-left: .1em; + } .fileList-header { align-items: center; display: flex; font-weight: bold; - margin: .5em calc(var(--default-horizontal-margin) / 2) 0; + height: 2.25em; + margin: 0 calc(var(--default-horizontal-margin) / 2); + padding: 0 .25em; } .rightControls { align-items: center; @@ -91,7 +110,13 @@ justify-content: flex-end; } .separator { - margin: 0 .25em; + background-color: rgba(0, 0, 0, .3); + height: 1.5em; + margin: 0 .4em; + width: 1px; + } + .separator.transparent { + background-color: transparent; } .expandInline { padding-right: .25em; @@ -102,46 +127,52 @@ .editLoaded .showOnEdit { display: initial; } + .label { + font-family: var(--font-family-bold); + margin-right: 1em; + } @media screen and (max-width: 50em) { - .desktop { + .patchInfo-header .desktop { display: none; } } </style> - <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchRange.patchNum, allPatchSets)]]"> + <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]"> <div class="patchInfo-header-wrapper"> - <div> + <div class="patchInfo-left"> + <h3 class="label">Files</h3> <gr-patch-range-select id="rangeSelect" comments="[[comments]]" change-num="[[changeNum]]" - patch-range="[[patchRange]]" + patch-num="{{patchNum}}" + base-patch-num="{{basePatchNum}}" available-patches="[[allPatchSets]]" revisions="[[change.revisions]]" on-patch-range-change="_handlePatchChange"> </gr-patch-range-select> - / + <span class="separator"></span> <gr-commit-info change="[[change]]" server-config="[[serverConfig]]" commit-info="[[commitInfo]]"></gr-commit-info> - <span class="latestPatchContainer"> - / + <span class="container latestPatchContainer"> + <span class="separator"></span> <a href$="[[changeUrl]]">Go to latest patch set</a> </span> - <span class="downloadContainer desktop"> - / + <span class="container downloadContainer desktop"> + <span class="separator"></span> <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button> </span> - <span class="descriptionContainer hideOnEdit"> - / + <span class="container descriptionContainer hideOnEdit"> + <span class="separator"></span> <gr-editable-label id="descriptionLabel" class="descriptionLabel" label-text="Add patchset description" - value="[[_computePatchSetDescription(change, patchRange.patchNum)]]" + value="[[_computePatchSetDescription(change, patchNum)]]" placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]" read-only="[[_descriptionReadOnly]]" on-changed="_handleDescriptionChanged"></gr-editable-label> @@ -158,7 +189,6 @@ </div> </div> <div class="fileList-header"> - <div>Files</div> <div class="rightControls"> <template is="dom-if" if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"> @@ -166,7 +196,7 @@ id="expandBtn" link on-tap="_expandAllDiffs">Show diffs</gr-button> - <span class="separator">/</span> + <span class="separator"></span> <gr-button id="collapseBtn" link @@ -178,7 +208,7 @@ Bulk actions disabled because there are too many files. </div> </template> - <span class="separator">/</span> + <span class="separator"></span> <gr-select id="modeSelect" bind-value="{{diffViewMode}}">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js index e1c7977..4b6d59c 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -38,8 +38,16 @@ type: String, notify: true, }, - /** @type {?} */ - patchRange: Object, + patchNum: { + type: String, + notify: true, + observer: '_patchOrBaseChanged', + }, + basePatchNum: { + type: String, + notify: true, + observer: '_patchOrBaseChanged', + }, revisions: Array, // Caps the number of files that can be shown and have the 'show diffs' / // 'hide diffs' buttons still be functional. @@ -67,7 +75,7 @@ }, _computeDescriptionPlaceholder(readOnly) { - return (readOnly ? 'No' : 'Add a') + ' patch set description'; + return (readOnly ? 'No' : 'Add') + ' patchset description'; }, _computeDescriptionReadOnly(loggedIn, change, account) { @@ -80,7 +88,6 @@ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; }, - /** * @param {!Object} revisions The revisions object keyed by revision hashes * @param {?Object} patchSet A revision already fetched from {revisions} @@ -98,10 +105,10 @@ _handleDescriptionChanged(e) { const desc = e.detail.trim(); const rev = this.getRevisionByPatchNum(this.change.revisions, - this.patchRange.patchNum); + this.patchNum); const sha = this._getPatchsetHash(this.change.revisions, rev); this.$.restAPI.setDescription(this.changeNum, - this.patchRange.patchNum, desc) + this.patchNum, desc) .then(res => { if (res.ok) { this.set(['_change', 'revisions', sha, 'description'], desc); @@ -137,35 +144,18 @@ this.findSortedIndex(basePatchNum, this.revisions); }, - /** - * Change active patch to the provided patch num. - * @param {number|string} basePatchNum the base patch to be viewed. - * @param {number|string} patchNum the patch number to be viewed. - * @param {boolean} opt_forceParams When set to true, the resulting URL will - * always include the patch range, even if the requested patchNum is - * known to be the latest. + /* + * Triggered by _patchNum and _basePatchNum observer, in order to detect if + * the patch has been previously set or not. The new patch number is not + * explicitly used, because this could be called by either _patchNum or + * _basePatchNum's observer. Since the behavior is the same, they are + * combined. */ - _changePatchNum(basePatchNum, patchNum, opt_forceParams) { - if (!opt_forceParams) { - let currentPatchNum; - if (this.change.current_revision) { - currentPatchNum = - this.change.revisions[this.change.current_revision]._number; - } else { - currentPatchNum = this.computeLatestPatchNum(this.allPatchSets); - } - if (this.patchNumEquals(patchNum, currentPatchNum) && - basePatchNum === 'PARENT') { - Gerrit.Nav.navigateToChange(this.change); - return; - } - } - Gerrit.Nav.navigateToChange(this.change, patchNum, - basePatchNum); - }, + _patchOrBaseChanged(patchNew, patchOld) { + if (!patchOld) { return; } - _handlePatchChange(e) { - this._changePatchNum(e.detail.leftPatch, e.detail.rightPatch, true); + Gerrit.Nav.navigateToChange(this.change, this.patchNum, + this.basePatchNum); }, _handlePrefsTap(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html index 62ae5c7..f2bf809 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -43,11 +43,9 @@ suite('gr-file-list-header tests', () => { let element; let sandbox; - let navigateToChangeStub; setup(() => { sandbox = sinon.sandbox.create(); - navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); stub('gr-rest-api-interface', { getConfig() { return Promise.resolve({test: 'config'}); }, getAccount() { return Promise.resolve(null); }, @@ -93,9 +91,9 @@ test('_computeDescriptionPlaceholder', () => { assert.equal(element._computeDescriptionPlaceholder(true), - 'No patch set description'); + 'No patchset description'); assert.equal(element._computeDescriptionPlaceholder(false), - 'Add a patch set description'); + 'Add patchset description'); }); test('_computePatchSetDisabled', () => { @@ -129,10 +127,9 @@ sandbox.stub(element, '_computeDescriptionReadOnly'); element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; + element.basePatchNum = 'PARENT'; + element.patchNum = 1; + element.change = { change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', revisions: { @@ -180,10 +177,8 @@ const computeSpy = sandbox.spy(element, '_fileListActionsVisible'); element._files = []; element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; + element.basePatchNum = 'PARENT'; + element.patchNum = '2'; element.shownFileCount = 1; flush(() => { assert.isTrue(computeSpy.lastCall.returnValue); @@ -206,21 +201,8 @@ assert.equal(select.nativeSelect.value, 'UNIFIED_DIFF'); }); - test('_changePatchNum called when range select changes', () => { - const leftPatch = 1; - const rightPatch = 2; - sandbox.stub(element, '_changePatchNum'); - element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch}); - assert.isTrue(element._changePatchNum.lastCall - .calledWithExactly(1, 2, true)); - }); - - test('include base patch when not parent', () => { - element.changeNum = '42'; - element.patchRange = { - basePatchNum: '2', - patchNum: '3', - }; + test('navigateToChange called when range select changes', () => { + const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); element.change = { change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', revisions: { @@ -232,16 +214,12 @@ status: 'NEW', labels: {}, }; - - element._changePatchNum(2, 13); - assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly( - element.change, 13, 2)); - - element.patchRange.basePatchNum = 'PARENT'; - - element._changePatchNum('PARENT', 3); - assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly( - element.change, 3, 'PARENT')); + element.basePatchNum = 1; + element.patchNum = 2; + element.patchNum = 3; + assert.equal(navigateToChangeStub.callCount, 1); + assert.isTrue(navigateToChangeStub.lastCall + .calledWithExactly(element.change, 3, 1)); }); test('class is applied to file list on old patch set', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html index a558dbe..f31841f 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,10 +14,10 @@ limitations under the License. --> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../../core/gr-reporting/gr-reporting.html"> <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html"> @@ -39,9 +39,10 @@ } .row { align-items: center; - border-top: 1px solid #eee; + border-top: 1px solid #ddd; display: flex; - padding: .1em .25em; + height: 2.25em; + padding: 0 var(--default-horizontal-margin); } :host(.loading) .row { opacity: .5; @@ -128,7 +129,7 @@ display: none; } label.show-hide { - color: #00f; + color: var(--color-link); cursor: pointer; display: block; font-size: .8em;
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 aed4f02..11a4a29 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
@@ -118,6 +118,7 @@ }, behaviors: [ + Gerrit.AsyncForeachBehavior, Gerrit.KeyboardShortcutBehavior, Gerrit.PatchSetBehavior, Gerrit.PathListBehavior, @@ -138,7 +139,7 @@ 'c': '_handleCKey', '[': '_handleLeftBracketKey', ']': '_handleRightBracketKey', - 'o enter': '_handleOKey', + 'o': '_handleOKey', 'n': '_handleNKey', 'p': '_handlePKey', 'r': '_handleRKey', @@ -146,6 +147,24 @@ 'esc': '_handleEscKey', }, + listeners: { + keydown: '_scopedKeydownHandler', + }, + + /** + * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard + * events must be scoped to a component level (e.g. `enter`) in order to not + * override native browser functionality. + * + * Context: Issue 7277 + */ + _scopedKeydownHandler(e) { + if (e.keyCode === 13) { + // Enter. + this._handleOKey(e); + } + }, + reload() { if (!this.changeNum || !this.patchRange.patchNum) { return Promise.resolve(); @@ -844,22 +863,20 @@ * @return {!Promise} */ _renderInOrder(paths, diffElements, initialCount) { - if (!paths.length) { + let iter = 0; + return this.asyncForeach(paths, path => { + iter++; + console.log('Expanding diff', iter, 'of', initialCount, ':', path); + const diffElem = this._findDiffByPath(path, diffElements); + diffElem.comments = this.$.commentAPI.getCommentsForPath(path, + this.patchRange, this.projectConfig); + const promises = [diffElem.reload()]; + if (this._isLoggedIn) { + promises.push(this._reviewFile(path)); + } + return Promise.all(promises); + }).then(() => { console.log('Finished expanding', initialCount, 'diff(s)'); - return Promise.resolve(); - } - console.log('Expanding diff', 1 + initialCount - paths.length, 'of', - initialCount, ':', paths[0]); - const diffElem = this._findDiffByPath(paths[0], diffElements); - diffElem.comments = this.$.commentAPI.getCommentsForPath(paths[0], - this.patchRange, this.projectConfig); - - const promises = [diffElem.reload()]; - if (this._isLoggedIn) { - promises.push(this._reviewFile(paths[0])); - } - return Promise.all(promises).then(() => { - return this._renderInOrder(paths.slice(1), diffElements, initialCount); }); },
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html index 36e2bc7..6496091 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -51,19 +51,24 @@ .selectedValueText.hidden { display: none; } - iron-selector > gr-button:first-of-type { - border-bottom-left-radius: 2px; - border-top-left-radius: 2px; - } - iron-selector > gr-button:last-of-type { - border-bottom-right-radius: 2px; - border-top-right-radius: 2px; - } - iron-selector > gr-button.iron-selected { - background-color: #ddd; - } gr-button { min-width: 40px; + --gr-button: { + border: 1px solid #d1d2d3; + border-radius: 12px; + box-shadow: none; + padding: .2em .85em; + } + --gr-button-background: #f5f5f5; + --gr-button-color: black; + --gr-button-hover-color: black; + + } + iron-selector > gr-button.iron-selected { + --gr-button-background:#ddd; + --gr-button-color: black; + --gr-button-hover-background-color: #ddd; + --gr-button-hover-color: black; } .placeholder { display: inline-block;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js index 812f0bd..dd0bccc 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -106,6 +106,8 @@ // nothing and then to the new item. if (!e.target.selectedItem) { return; } this._selectedValueText = e.target.selectedItem.getAttribute('title'); + // Needed to update the style of the selected button. + this.updateStyles(); this.fire('labels-changed'); },
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html index c0e55ef..845bdaf 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -28,7 +28,7 @@ <template> <style include="shared-styles"> :host { - border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; display: block; position: relative; cursor: pointer;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html index 2eed88b..0fa6f4d 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -28,11 +28,15 @@ display: block; } .header { + align-items: center; + background-color: #fafafa; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; display: flex; justify-content: space-between; - margin-bottom: .35em; + min-height: 3.2em; + padding: .5em var(--default-horizontal-margin); } - .header, #messageControlsContainer { padding: 0 var(--default-horizontal-margin); } @@ -45,25 +49,40 @@ } #messageControlsContainer { align-items: center; - background-color: #fef; + border-bottom: 1px solid #ddd; display: flex; + height: 2.25em; justify-content: center; } #messageControlsContainer gr-button { - padding: 0.4em; + padding: 0.4em 0; + } + .separator { + background-color: rgba(0, 0, 0, .3); + height: 1.5em; + margin: 0 .4em; + width: 1px; + } + .separator.transparent { + background-color: transparent; + } + .container { + align-items: center; + display: flex; } </style> <div class="header"> <h3>Messages</h3> - <div class="messageListControls"> + <div class="messageListControls container"> <gr-button id="collapse-messages" link on-tap="_handleExpandCollapseTap"> [[_computeExpandCollapseMessage(_expanded)]] </gr-button> <span id="automatedMessageToggleContainer" + class="container" hidden$="[[!_hasAutomatedMessages(messages)]]"> - / + <span class="transparent separator"></span> <gr-button id="automatedMessageToggle" link on-tap="_handleAutomatedMessageToggleTap"> [[_computeAutomatedToggleText(_hideAutomated)]] @@ -78,8 +97,9 @@ [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]] </gr-button> <span + class="container" hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]"> - / + <span class="transparent separator"></span> <gr-button id="incrementMessagesBtn" link on-tap="_handleIncrementShownMessages"> [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html index c47d63f..15964a7 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -86,14 +86,14 @@ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); assert.equal(getMessages().length, 20); - assert.equal(element.$.incrementMessagesBtn.innerText.trim(), - 'Show 5 more'); + assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase() + .trim(), 'SHOW 5 MORE'); MockInteractions.tap(element.$.incrementMessagesBtn); flushAsynchronousOperations(); assert.equal(getMessages().length, 25); - assert.equal(element.$.incrementMessagesBtn.innerText.trim(), - 'Show 1 more'); + assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase() + .trim(), 'SHOW 1 MORE'); MockInteractions.tap(element.$.incrementMessagesBtn); flushAsynchronousOperations(); @@ -108,7 +108,8 @@ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); assert.equal(getMessages().length, 20); - assert.equal(element.$.oldMessagesBtn.innerText, 'Show all 6 messages'); + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW ALL 6 MESSAGES'); MockInteractions.tap(element.$.oldMessagesBtn); flushAsynchronousOperations(); @@ -121,7 +122,8 @@ .concat(_.times(11, randomMessage)); flushAsynchronousOperations(); - assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message'); + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW 1 MESSAGE'); assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); MockInteractions.tap(element.$.automatedMessageToggle); flushAsynchronousOperations(); @@ -134,12 +136,14 @@ .concat(_.times(11, randomAutomated)); flushAsynchronousOperations(); - assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message'); + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW 1 MESSAGE'); assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); MockInteractions.tap(element.$.automatedMessageToggle); flushAsynchronousOperations(); - assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message'); + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW 1 MESSAGE'); assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html index 161dfe7..4b02c3d 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -117,10 +117,6 @@ .draftsContainer h3 { margin-top: .25em; } - .action:link, - .action:visited { - color: #00e; - } #checkingStatusLabel, #notLatestLabel { margin-left: 1em;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html index 09f3029..9f5d39d 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -67,7 +67,9 @@ .linksTitle { color: var(--primary-text-color); display: inline-block; + font-family: var(--font-family-bold); position: relative; + text-transform: uppercase; } .linksTitle:hover { opacity: .75; @@ -157,7 +159,9 @@ </li> </template> <li> - <a class="browse linksTitle" href$="[[_computeRelativeURL('/admin/projects')]]"> + <a + class="browse linksTitle" + href$="[[_computeRelativeURL('/admin/projects')]]"> Browse</a> </li> </ul>
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 84bd586..8ae5617 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
@@ -59,6 +59,7 @@ padding: .75em var(--default-horizontal-margin); } .patchRangeLeft { + align-items: center; display: flex; } .navLink:not([href]), @@ -79,14 +80,7 @@ .mobile { display: none; } - .downArrow { - display: inline-block; - font-size: .6em; - user-select: none; - vertical-align: middle; - } .dropdown-trigger { - color: #00e; cursor: pointer; padding: 0; } @@ -112,7 +106,7 @@ width: .3em; } .dropdown-content a:hover { - background-color: #00e; + background-color: var(--color-link); color: #fff; } .dropdown-content a[selected] { @@ -126,7 +120,6 @@ color: #000; } gr-button { - font: inherit; padding: .3em 0; text-decoration: none; } @@ -229,9 +222,14 @@ on-change="_handleReviewedChange" hidden$="[[!_loggedIn]]" hidden> <div class="jumpToFileContainer desktop"> - <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> + <gr-button + down-arrow + no-uppercase + link + class="dropdown-trigger" + id="trigger" + on-tap="_showDropdownTapHandler"> <span>[[computeDisplayPath(_path)]]</span> - <span class="downArrow">▼</span> </gr-button> <!-- *-align="" to disable iron-dropdown's element positioning. --> <iron-dropdown id="dropdown" @@ -283,11 +281,11 @@ <gr-patch-range-select id="rangeSelect" change-num="[[_changeNum]]" - patch-range="[[_patchRange]]" + patch-num="{{_patchNum}}" + base-patch-num="{{_basePatchNum}}" files-weblinks="[[_filesWeblinks]]" - available-patches="[[_computeAvailablePatches(_change.revisions, _change.revisions.*)]]" - revisions="[[_change.revisions]]" - on-patch-range-change="_handlePatchChange"> + available-patches="[[_allPatchSets]]" + revisions="[[_change.revisions]]"> </gr-patch-range-select> <span class="download desktop"> <span class="separator">/</span>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js index ee12e34..c4746f2 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -63,6 +63,22 @@ }, _patchRange: Object, + // These are kept as separate properties from the patchRange so that the + // observer can be aware of the previous value. In order to view sub + // property changes for _patchRange, a complex observer must be used, and + // that only displays the new value. + // + // If a previous value did not exist, the change is not reloaded with the + // new patches. This is just the initial setting from the change view vs. + // an update coming from the two way data binding. + _patchNum: { + type: String, + observer: '_patchOrBaseChanged', + }, + _basePatchNum: { + type: String, + observer: '_patchOrBaseChanged', + }, /** * @type {{ * subject: string, @@ -124,7 +140,6 @@ type: Boolean, computed: '_computeEditLoaded(_patchRange.*)', }, - _isBlameSupported: { type: Boolean, value: false, @@ -134,6 +149,10 @@ type: Boolean, value: false, }, + _allPatchSets: { + type: Array, + computed: 'computeAllPatchSets(_change, _change.revisions.*)', + }, }, behaviors: [ @@ -147,6 +166,7 @@ '_getProjectConfig(_change.project)', '_getFiles(_changeNum, _patchRange.*)', '_setReviewedObserver(_loggedIn, params.*)', + '_patchRangeChanged(_patchRange.*)', ], keyBindings: { @@ -563,6 +583,17 @@ this.$.cursor.initialLineNumber = params.lineNum; }, + _patchRangeChanged() { + this._basePatchNum = this._patchRange.basePatchNum; + this._patchNum = this._patchRange.patchNum; + }, + + _patchOrBaseChanged(patchNew, patchOld) { + if (!patchOld) { return; } + + this._handlePatchChange(this._basePatchNum, this._patchNum); + }, + _pathChanged(path) { if (path) { this.fire('title-change', @@ -593,12 +624,6 @@ return patchStr; }, - _computeAvailablePatches(revs) { - return this.sortRevisions(Object.values(revs)).map(e => { - return {num: e._number}; - }); - }, - /** * When the latest patch of the change is selected (and there is no base * patch) then the patch range need not appear in the URL. Return a patch @@ -675,11 +700,9 @@ this.$.dropdown.open(); }, - _handlePatchChange(e) { - const rightPatch = e.detail.rightPatch; - const leftPatch = e.detail.leftPatch; + _handlePatchChange(basePatchNum, patchNum) { Gerrit.Nav.navigateToDiff( - this._change, this._path, rightPatch, leftPatch); + this._change, this._path, patchNum, basePatchNum); }, _handlePrefsTap(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html index ae1404a..8587fe5 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -163,6 +163,7 @@ _number: 42, revisions: { a: {_number: 10}, + b: {_number: 5}, }, }; element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; @@ -438,15 +439,23 @@ }); test('_handlePatchChange calls navigateToDiff correctly', () => { - const leftPatch = 'PARENT'; - const rightPatch = '3'; const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); element._change = {_number: 321, project: 'foo/bar'}; element._path = 'path/to/file.txt'; - element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch}); + + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: '3', + }; + + assert.equal(element._basePatchNum, element._patchRange.basePatchNum); + assert.equal(element._patchNum, element._patchRange.patchNum); + + element._patchNum = '1'; + assert(navigateStub.lastCall.calledWithExactly(element._change, - element._path, rightPatch, leftPatch)); + element._path, '1', 'PARENT')); }); test('download link', () => { @@ -759,6 +768,7 @@ }; test('reviewed checkbox', () => { + sandbox.stub(element, '_handlePatchChange'); element._patchRange = {patchNum: '1'}; // Reviewed checkbox should be shown. assert.isTrue(isVisible(element.$.reviewed));
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html index a228a83..ab52e15e 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -13,46 +13,39 @@ 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="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> <link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html"> + +<link rel="import" href="../../shared/gr-select/gr-select.html"> <dom-module id="gr-patch-range-select"> <template> <style include="shared-styles"> - .patchRange { - display: inline-block; + :host { + align-items: center; + display: flex; } select { max-width: 15em; } + .arrow { + margin: 0 .5em; + } @media screen and (max-width: 50em) { .filesWeblinks { display: none; } - select { - max-width: 5.25em; - } } </style> - Patch set: <span class="patchRange"> - <gr-select id="leftPatchSelect" bind-value="{{_leftSelected}}" - on-change="_handlePatchChange"> - <select> - <option value="PARENT">Base</option> - <template is="dom-repeat" items="{{availablePatches}}" as="basePatchNum"> - <option value$="[[basePatchNum.num]]" - disabled$="[[_computeLeftDisabled(basePatchNum.num, patchRange.patchNum, _sortedRevisions)]]"> - [[basePatchNum.num]] - [[_computePatchSetCommentsString(comments, basePatchNum.num)]] - [[_computePatchSetDescription(revisions, basePatchNum.num)]] - </option> - </template> - </select> - </gr-select> + <gr-dropdown-list + id="basePatchDropdown" + value="{{basePatchNum}}" + items="[[_baseDropdownContent]]"> + </gr-dropdown-list> </span> <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks"> <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink"> @@ -60,21 +53,13 @@ href$="[[weblink.url]]">[[weblink.name]]</a> </template> </span> - → + <span class="arrow">→</span> <span class="patchRange"> - <gr-select id="rightPatchSelect" bind-value="{{_rightSelected}}" - on-change="_handlePatchChange"> - <select> - <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> - <option value$="[[patchNum.num]]" - disabled$="[[_computeRightDisabled(patchNum.num, patchRange.basePatchNum, _sortedRevisions)]]"> - [[patchNum.num]] - [[_computePatchSetCommentsString(comments, patchNum.num)]] - [[_computePatchSetDescription(revisions, patchNum.num)]] - </option> - </template> - </select> - </gr-select> + <gr-dropdown-list + id="patchNumDropdown" + value="{{patchNum}}" + items="[[_patchDropdownContent]]"> + </gr-dropdown-list> <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks"> <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink"> <a target="_blank"
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js index f6b759e..4ca9fb9 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -31,28 +31,89 @@ properties: { availablePatches: Array, + _baseDropdownContent: { + type: Object, + computed: '_computeBaseDropdownContent(availablePatches, patchNum,' + + '_sortedRevisions, revisions)', + }, + _patchDropdownContent: { + type: Object, + computed: '_computePatchDropdownContent(availablePatches,' + + 'basePatchNum, _sortedRevisions, revisions)', + }, changeNum: String, comments: Array, /** @type {{ meta_a: !Array, meta_b: !Array}} */ filesWeblinks: Object, - /** @type {?} */ - patchRange: Object, + patchNum: { + type: String, + notify: true, + }, + basePatchNum: { + type: String, + notify: true, + }, revisions: Object, _sortedRevisions: Array, - _rightSelected: String, - _leftSelected: String, }, observers: [ '_updateSortedRevisions(revisions.*)', - '_updateSelected(patchRange.*)', ], behaviors: [Gerrit.PatchSetBehavior], - _updateSelected() { - this._rightSelected = this.patchRange.patchNum; - this._leftSelected = this.patchRange.basePatchNum; + _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions, + revisions) { + const dropdownContent = []; + dropdownContent.push({ + text: 'Base', + value: 'PARENT', + }); + for (const basePatch of availablePatches) { + const basePatchNum = basePatch.num; + dropdownContent.push({ + disabled: this._computeLeftDisabled( + basePatch.num, patchNum, _sortedRevisions), + triggerText: `Patchset ${basePatchNum}`, + text: `Patchset ${basePatchNum}` + + this._computePatchSetCommentsString(this.comments, basePatchNum), + mobileText: this._computeMobileText(basePatchNum, this.comments, + revisions), + bottomText: `${this._computePatchSetDescription( + revisions, basePatchNum)}`, + value: basePatch.num, + }); + } + return dropdownContent; + }, + + _computeMobileText(patchNum, comments, revisions) { + return `${patchNum}` + + `${this._computePatchSetCommentsString(this.comments, patchNum)}` + + `${this._computePatchSetDescription(revisions, patchNum, true)}`; + }, + + _computePatchDropdownContent(availablePatches, basePatchNum, + _sortedRevisions, revisions) { + const dropdownContent = []; + for (const patch of availablePatches) { + const patchNum = patch.num; + dropdownContent.push({ + disabled: this._computeRightDisabled(patchNum, basePatchNum, + _sortedRevisions), + triggerText: `Patchset ${patchNum}`, + text: `Patchset ${patchNum}` + + `${this._computePatchSetCommentsString( + this.comments, patchNum)}`, + mobileText: this._computeMobileText(patchNum, this.comments, + revisions), + bottomText: `${this._computePatchSetDescription( + revisions, patchNum)}`, + value: patchNum, + }); + } + return dropdownContent; }, _updateSortedRevisions(revisionsRecord) { @@ -60,13 +121,6 @@ this._sortedRevisions = this.sortRevisions(Object.values(revisions)); }, - _handlePatchChange(e) { - const leftPatch = this._leftSelected; - const rightPatch = this._rightSelected; - this.fire('patch-range-change', {rightPatch, leftPatch}); - e.target.blur(); - }, - _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) { return this.findSortedIndex(basePatchNum, sortedRevisions) >= this.findSortedIndex(patchNum, sortedRevisions); @@ -85,11 +139,11 @@ // debounce these, but because they are detecting two different // events, sometimes the timing was off and one ended up missing. _synchronizeSelectionRight() { - this.$.rightPatchSelect.value = this._rightSelected; + this.$.rightPatchSelect.value = this.patchNum; }, _synchronizeSelectionLeft() { - this.$.leftPatchSelect.value = this._leftSelected; + this.$.leftPatchSelect.value = this.basePatchNum; }, // Copied from gr-file-list @@ -145,7 +199,7 @@ } let commentsStr = ''; if (numComments > 0) { - commentsStr = '(' + numComments + ' comments'; + commentsStr = ' (' + numComments + ' comments'; if (numUnresolved > 0) { commentsStr += ', ' + numUnresolved + ' unresolved'; } @@ -154,9 +208,15 @@ return commentsStr; }, - _computePatchSetDescription(revisions, patchNum) { + /** + * @param {!Array} revisions + * @param {number|string} patchNum + * @param {boolean=} opt_addFrontSpace + */ + _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) { const rev = this.getRevisionByPatchNum(revisions, patchNum); return (rev && rev.description) ? + (opt_addFrontSpace ? ' ' : '') + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; }, });
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html index 92553a0..d49974b 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -79,19 +79,74 @@ patchRange.basePatchNum, sortedRevisions)); }); - test('_updateSelected called with subproperty changes', () => { - sandbox.stub(element, '_updateSelected'); - element.patchRange = {patchNum: 1, basePatchNum: 'PARENT'}; - assert.equal(element._updateSelected.callCount, 1); - - element.set('patchRange.patchNum', 2); - assert.equal(element._updateSelected.callCount, 2); - - element.set('patchRange.basePatchNum', 1); - assert.equal(element._updateSelected.callCount, 3); + test('_computeBaseDropdownContent', () => { + element.comments = {}; + const availablePatches = [ + {num: 1}, + {num: 2}, + {num: 3}, + {num: 'edit'}, + ]; + const revisions = [ + { + commit: {}, + _number: 2, + description: 'description', + }, + {commit: {}}, + {commit: {}}, + {commit: {}}, + ]; + const patchNum = 1; + const sortedRevisions = [ + {_number: 1}, + {_number: 2}, + {_number: element.EDIT_NAME, basePatchNum: 2}, + {_number: 3}, + ]; + const expectedResult = [ + { + text: 'Base', + value: 'PARENT', + }, + { + disabled: true, + triggerText: 'Patchset 1', + text: 'Patchset 1', + mobileText: '1', + bottomText: '', + value: 1, + }, + { + disabled: true, + triggerText: 'Patchset 2', + text: 'Patchset 2', + mobileText: '2 description', + bottomText: 'description', + value: 2, + }, + { + disabled: true, + triggerText: 'Patchset 3', + text: 'Patchset 3', + mobileText: '3', + bottomText: '', + value: 3, + }, + { + disabled: true, + triggerText: 'Patchset edit', + text: 'Patchset edit', + mobileText: 'edit', + bottomText: '', + value: 'edit', + }, + ]; + assert.deepEqual(element._computeBaseDropdownContent(availablePatches, + patchNum, sortedRevisions, revisions), expectedResult); }); - test('_computeLeftDisabled called when patchNum updates', () => { + test('_computeBaseDropdownContent called when patchNum updates', () => { element.revisions = [ {commit: {}}, {commit: {}}, @@ -104,118 +159,103 @@ {num: 3}, {num: 'edit'}, ]; - element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'}; + element.patchNum = 2; + element.basePatchNum = 'PARENT'; + flushAsynchronousOperations(); + + sandbox.stub(element, '_computeBaseDropdownContent'); + + // Should be recomputed for each available patch + element.set('patchNum', 1); + assert.equal(element._computeBaseDropdownContent.callCount, 1); + }); + + test('_computePatchDropdownContent called when basePatchNum updates', () => { + element.revisions = [ + {commit: {}}, + {commit: {}}, + {commit: {}}, + {commit: {}}, + ]; + element.availablePatches = [ + {num: 1}, + {num: 2}, + {num: 3}, + {num: 'edit'}, + ]; + element.patchNum = 2; + element.basePatchNum = 'PARENT'; flushAsynchronousOperations(); // Should be recomputed for each available patch - sandbox.stub(element, '_computeLeftDisabled'); - element.set('patchRange.patchNum', '1'); - assert.equal(element._computeLeftDisabled.callCount, 4); + sandbox.stub(element, '_computePatchDropdownContent'); + element.set('basePatchNum', 1); + assert.equal(element._computePatchDropdownContent.callCount, 1); }); - test('_computeRightDisabled called when basePatchNum updates', () => { - element.revisions = [ - {commit: {}}, - {commit: {}}, - {commit: {}}, - {commit: {}}, - ]; - element.availablePatches = [ + test('_computePatchDropdownContent', () => { + element.comments = {}; + const availablePatches = [ {num: 1}, {num: 2}, {num: 3}, {num: 'edit'}, ]; - element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'}; - flushAsynchronousOperations(); - - // Should be recomputed for each available patch - sandbox.stub(element, '_computeRightDisabled'); - element.set('patchRange.basePatchNum', '1'); - assert.equal(element._computeRightDisabled.callCount, 4); - }); - - - test('changes in patch range fire event', done => { - sandbox.stub(element, '_computeLeftDisabled').returns(false); - sandbox.stub(element, '_computeRightDisabled').returns(false); - const patchRangeChangedStub = sandbox.stub(); - element.addEventListener('patch-range-change', patchRangeChangedStub); - - const leftSelectEl = element.$.leftPatchSelect; - const rightSelectEl = element.$.rightPatchSelect; - const blurSpy = sandbox.spy(leftSelectEl, 'blur'); - element.changeNum = '42'; - element.path = 'path/to/file.txt'; - element.availablePatches = - [{num: '1'}, {num: '2'}, {num: '3'}, {num: 'edit'}]; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '3', - }; - flushAsynchronousOperations(); - - let numEvents = 0; - leftSelectEl.addEventListener('change', e => { - numEvents++; - if (numEvents === 1) { - assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail, - {rightPatch: '3', leftPatch: 'PARENT'}); - leftSelectEl.nativeSelect.value = 'edit'; - element.fire('change', {}, {node: leftSelectEl}); - assert(blurSpy.called, 'Dropdown should be blurred after selection'); - } else if (numEvents === 2) { - assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail, - {rightPatch: '3', leftPatch: 'edit'}); - rightSelectEl.nativeSelect.value = '1'; - element.fire('change', {}, {node: rightSelectEl}); - } - }); - rightSelectEl.addEventListener('change', e => { - assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail, - {rightPatch: '1', leftPatch: 'edit'}); - done(); - }); - leftSelectEl.nativeSelect.value = 'PARENT'; - rightSelectEl.nativeSelect.value = '3'; - element.fire('change', {}, {node: leftSelectEl}); - }); - - test('diff against dropdown', done => { - element.revisions = [ - {commit: {}}, + const revisions = [ + { + commit: {}, + _number: 2, + description: 'description', + }, {commit: {}}, {commit: {}}, {commit: {}}, ]; - element.availablePatches = [ - {num: 1}, - {num: 2}, - {num: 3}, - {num: 'edit'}, + const basePatchNum = 1; + const sortedRevisions = [ + {_number: 1}, + {_number: 2}, + {_number: element.EDIT_NAME, basePatchNum: 2}, + {_number: 3}, ]; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '3', - }; - const patchRangeChangedStub = sandbox.stub(); - element.addEventListener('patch-range-change', patchRangeChangedStub); + const expectedResult = [ + { + disabled: true, + triggerText: 'Patchset 1', + text: 'Patchset 1', + mobileText: '1', + bottomText: '', + value: 1, + }, + { + disabled: false, + triggerText: 'Patchset 2', + text: 'Patchset 2', + mobileText: '2 description', + bottomText: 'description', + value: 2, + }, + { + disabled: false, + triggerText: 'Patchset 3', + text: 'Patchset 3', + mobileText: '3', + bottomText: '', + value: 3, + }, + { + disabled: false, + triggerText: 'Patchset edit', + text: 'Patchset edit', + mobileText: 'edit', + bottomText: '', + value: 'edit', + }, + ]; - flush(() => { - const selectEl = element.$.leftPatchSelect; - assert.equal(selectEl.nativeSelect.value, 'PARENT'); - assert.isTrue(element.$$('#leftPatchSelect option[value="3"]') - .hasAttribute('disabled')); - selectEl.addEventListener('change', () => { - assert.equal(selectEl.nativeSelect.value, 'edit'); - assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail, - {leftPatch: 'edit', rightPatch: '3'}); - done(); - }); - selectEl.nativeSelect.value = 'edit'; - element.fire('change', {}, {node: selectEl.nativeSelect}); - }); + assert.deepEqual(element._computePatchDropdownContent(availablePatches, + basePatchNum, sortedRevisions, revisions), expectedResult); }); test('filesWeblinks', () => { @@ -264,12 +304,12 @@ }; assert.equal(element._computePatchSetCommentsString(comments, 1), - '(3 comments, 1 unresolved)'); + ' (3 comments, 1 unresolved)'); // Test string with no unresolved comments. delete comments['foo']; assert.equal(element._computePatchSetCommentsString(comments, 1), - '(2 comments)'); + ' (2 comments)'); // Test string with no comments. delete comments['bar'];
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js index bfd8e90..ca3cf62 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
@@ -26,6 +26,7 @@ // NOTE: intended singleton. value: { + configured: false, loading: false, callbacks: [], }, @@ -60,12 +61,13 @@ }, _getHighlightLib() { - return window.hljs; - }, + const lib = window.hljs; + if (lib && !this._state.configured) { + this._state.configured = true; - _configureHighlightLib() { - this._getHighlightLib().configure( - {classPrefix: 'gr-diff gr-syntax gr-syntax-'}); + lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'}); + } + return lib; }, _getLibRoot() { @@ -93,10 +95,8 @@ } script.src = src; - script.onload = function() { - this._configureHighlightLib(); - resolve(); - }.bind(this); + script.onload = resolve; + script.onerror = reject; Polymer.dom(document.head).appendChild(script); }); },
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html index 6ddde46..6e88ed1 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
@@ -55,6 +55,7 @@ loadStub.restore(); // Because the element state is a singleton, clean it up. + element._state.configured = false; element._state.loading = false; element._state.callbacks = []; }); @@ -88,8 +89,13 @@ }); suite('preloaded', () => { + let hljsStub; + setup(() => { - window.hljs = 'test-object'; + hljsStub = { + configure: sinon.stub(), + }; + window.hljs = hljsStub; }); teardown(() => { @@ -101,7 +107,14 @@ element.get().then(firstCallHandler); flush(() => { assert.isTrue(firstCallHandler.called); - assert.isTrue(firstCallHandler.calledWith('test-object')); + assert.isTrue(firstCallHandler.calledWith(hljsStub)); + done(); + }); + }); + + test('configures hljs', done => { + element.get().then(() => { + assert.isTrue(window.hljs.configure.calledOnce); done(); }); });
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html index 31a276f..311a5af 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -60,7 +60,7 @@ color: #219; } .gr-syntax-type { - color: #00f; + color: var(--color-link); } .gr-syntax-title { color: #0000C0;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html index 578989e..c7ab3d9 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -24,6 +24,8 @@ <link rel="import" href="gr-endpoint-decorator.html"> <link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html"> +<script>void(0);</script> + <test-fixture id="basic"> <template> <gr-endpoint-decorator name="foo">
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js index 3a0c898..86c0961 100644 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -47,12 +47,10 @@ * States that it expects no more than 3 parameters, but that's not true. * @todo (beckysiegel) check Polymer annotations and submit change. */ - _importHtmlPlugins(plugins) { for (const url of plugins) { this.importHref( - this._urlFor(url), Gerrit._pluginInstalled, Gerrit._pluginInstalled, - true); + this._urlFor(url), null, Gerrit._pluginInstalled, true); } },
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html index 27adbe1..66c7511 100644 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -23,6 +23,8 @@ <link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-plugin-host.html"> +<script>void(0);</script> + <test-fixture id="basic"> <template> <gr-plugin-host></gr-plugin-host> @@ -61,9 +63,9 @@ plugin: {html_resource_paths: ['foo/bar', 'baz']}, }; assert.isTrue(element.importHref.calledWith( - '/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true)); + '/foo/bar', null, Gerrit._pluginInstalled, true)); assert.isTrue(element.importHref.calledWith( - '/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true)); + '/baz', null, Gerrit._pluginInstalled, true)); }); test('imports relative html plugins from config with a base url', () => { @@ -71,11 +73,9 @@ element.config = { plugin: {html_resource_paths: ['foo/bar', 'baz']}}; assert.isTrue(element.importHref.calledWith( - '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, - true)); + '/the-base/foo/bar', null, Gerrit._pluginInstalled, true)); assert.isTrue(element.importHref.calledWith( - '/the-base/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, - true)); + '/the-base/baz', null, Gerrit._pluginInstalled, true)); }); test('imports absolute html plugins from config', () => { @@ -88,11 +88,9 @@ }, }; assert.isTrue(element.importHref.calledWith( - 'http://example.com/foo/bar', Gerrit._pluginInstalled, - Gerrit._pluginInstalled, true)); + 'http://example.com/foo/bar', null, Gerrit._pluginInstalled, true)); assert.isTrue(element.importHref.calledWith( - 'https://example.com/baz', Gerrit._pluginInstalled, - Gerrit._pluginInstalled, true)); + 'https://example.com/baz', null, Gerrit._pluginInstalled, true)); }); test('adds js plugins from config to the body', () => { @@ -139,9 +137,9 @@ }, }; assert.isTrue(element.importHref.calledWith( - '/oof', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true)); + '/oof', null, Gerrit._pluginInstalled, true)); assert.isTrue(element.importHref.calledWith( - '/some', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true)); + '/some', null, Gerrit._pluginInstalled, true)); }); }); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html index fa3428a..6805885 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -59,18 +59,21 @@ <td class="urlCell">[[item.url]]</td> <td class="buttonColumn"> <gr-button + link data-index="[[index]]" on-tap="_handleMoveUpButton" class="moveUpButton">↑</gr-button> </td> <td class="buttonColumn"> <gr-button + link data-index="[[index]]" on-tap="_handleMoveDownButton" class="moveDownButton">↓</gr-button> </td> <td> <gr-button + link data-index="[[index]]" on-tap="_handleDeleteButton" class="remove-button">Delete</gr-button> @@ -99,6 +102,7 @@ <th></th> <th> <gr-button + link disabled$="[[_computeAddDisabled(_newName, _newUrl)]]" on-tap="_handleAddButton">Add</gr-button> </th>
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 cacccda..7764d3b 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
@@ -353,6 +353,7 @@ id="emailEditor" has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor> <gr-button + link on-tap="_handleSaveEmails" disabled$="[[!_emailsChanged]]">Save changes</gr-button> </fieldset>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html index a834ea2..68b2c23 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -83,6 +83,7 @@ </template> <td> <gr-button + link data-index$="[[projectIndex]]" on-tap="_handleRemoveProject">Delete</gr-button> </td> @@ -106,7 +107,7 @@ placeholder="branch:name, or other search expression"> </th> <th> - <gr-button on-tap="_handleAddProject">Add</gr-button> + <gr-button link on-tap="_handleAddProject">Add</gr-button> </th> </tr> </tfoot>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html index 90594de..b658025 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html +++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -37,24 +37,31 @@ :host([show-avatar]) .container { padding-left: 0; } - gr-button.remove, gr-button.remove:hover, gr-button.remove:focus { - border-color: transparent; - color: #333; + --gr-button: { + color: #333; + } } gr-button.remove { - background: #eee; - border: 0; - color: #666; - font-size: 1.7em; - font-weight: normal; - height: .6em; - line-height: .6em; - margin-left: .15em; - margin-top: -.05em; - padding: 0; - text-decoration: none; + --gr-button: { + border: 0; + color: #666; + font-size: 1.7em; + font-weight: normal; + height: .6em; + line-height: .6em; + margin-left: .15em; + margin-top: -.05em; + padding: 0; + text-decoration: none; + } + --gr-button-hover-color: { + color: #333; + } + --gr-button-hover-background-color: { + color: #333; + } } :host:focus { border-color: transparent; @@ -78,6 +85,7 @@ <gr-account-link account="[[account]]"></gr-account-link> <gr-button id="remove" + link hidden$="[[!removable]]" hidden tabindex="-1"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html index dcb38d4..c1c0338 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -15,7 +15,9 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> + <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> <script src="../../../scripts/rootElement.js"></script> <link rel="import" href="../../../styles/shared-styles.html"> @@ -23,12 +25,6 @@ <dom-module id="gr-autocomplete-dropdown"> <template> <style include="shared-styles"> - :host { - background: #fff; - box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; - position: absolute; - z-index: 104; - } /* This must be set here vs. the container component because in some cases the element is moved in the DOM to a base element and is no longer a child of its original parent. */ @@ -48,19 +44,34 @@ li.selected { background-color: #eee; } + .dropdown-content { + background: #fff; + box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; + } </style> - <div id="suggestions" role="listbox"> - <ul> - <template is="dom-repeat" items="[[suggestions]]"> - <li data-index$="[[index]]" - data-value$="[[item.dataValue]]" - tabindex="-1" - aria-label$="[[item.name]]" - role="option" - on-tap="_handleTapItem">[[item.text]]</li> - </template> - </ul> - </div> + <iron-dropdown + id="dropdown" + allow-outside-scroll="true" + vertical-align="top" + horizontal-align="auto" + vertical-offset="[[verticalOffset]]"> + <div + class="dropdown-content" + slot="dropdown-content" + id="suggestions" + role="listbox"> + <ul> + <template is="dom-repeat" items="[[suggestions]]"> + <li data-index$="[[index]]" + data-value$="[[item.dataValue]]" + tabindex="-1" + aria-label$="[[item.name]]" + role="option" + on-tap="_handleTapItem">[[item.text]]</li> + </template> + </ul> + </div> + </iron-dropdown> <gr-cursor-manager id="cursor" index="{{index}}"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js index 12fb074..100b5ca 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,6 +14,9 @@ (function() { 'use strict'; + const AWAIT_MAX_ITERS = 10; + const AWAIT_STEP = 5; + Polymer({ is: 'gr-autocomplete-dropdown', @@ -31,8 +34,14 @@ properties: { index: Number, - moveToRoot: Boolean, - fixedPosition: Boolean, + verticalOffset: { + type: Number, + value: null, + }, + horizontalOffset: { + type: Number, + value: null, + }, suggestions: { type: Array, observer: '_resetCursorStops', @@ -55,31 +64,47 @@ tab: '_handleTab', }, - attached() { - if (this.fixedPosition) { - this.classList.add('fixed'); - } - }, - close() { - if (this.moveToRoot) { - Gerrit.getRootElement().removeChild(this); - } else { - this.hidden = true; - } + this.$.dropdown.close(); }, open() { - if (this.moveToRoot) { - Gerrit.getRootElement().appendChild(this); - } - this._resetCursorStops(); - this._resetCursorIndex(); + this._open().then(() => { + this._resetCursorStops(); + this._resetCursorIndex(); + this.fire('open-complete'); + }); }, - setPosition(top, left) { - this.style.top = top; - this.style.left = left; + // TODO (beckysiegel) look into making this a behavior since it's used + // 3 times now. + _open(...args) { + return new Promise(resolve => { + Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args); + this._awaitOpen(resolve); + }); + }, + + /** + * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually + * opening. Eventually replace with a direct way to listen to the overlay. + */ + _awaitOpen(fn) { + let iters = 0; + const step = () => { + this.async(() => { + if (this.style.display !== 'none') { + fn.call(this); + } else if (iters++ < AWAIT_MAX_ITERS) { + step.call(this); + } + }, AWAIT_STEP); + }; + step.call(this); + }, + + get isHidden() { + return !this.$.dropdown.opened; }, getCurrentText() { @@ -134,9 +159,7 @@ _handleEscape() { this._fireClose(); - if (!this.hidden) { - this.close(); - } + this.close(); }, _handleTapItem(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html index db9440c..23b27be 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -27,13 +27,7 @@ <test-fixture id="basic"> <template> - <gr-autocomplete-dropdown id="dropdown"></gr-autocomplete-dropdown> - </template> -</test-fixture> - -<test-fixture id="move"> - <template> - <gr-autocomplete-dropdown id="dropdown" move-to-root></gr-autocomplete-dropdown> + <gr-autocomplete-dropdown></gr-autocomplete-dropdown> </template> </test-fixture> @@ -49,6 +43,7 @@ element.suggestions = [ {dataValue: 'test value 1', name: 'test name 1', text: 1}, {dataValue: 'test value 2', name: 'test name 2', text: 2}]; + flushAsynchronousOperations(); }); teardown(() => { @@ -56,26 +51,12 @@ if (element.isOpen) element.close(); }); - test('dropdown has not been moved from text fixture to the body', () => { - assert.equal(Polymer.dom(document.root) - .querySelectorAll('gr-autocomplete-dropdown').length, 1); - const dropdown = Polymer.dom(document.root) - .querySelector('gr-autocomplete-dropdown'); - assert.isOk(dropdown); - assert.notDeepEqual(dropdown.parentElement, - Polymer.dom(document.root).querySelector('body')); - }); - - test('escape key', () => { - const listener = sandbox.spy(); - element.hidden = false; - element.addEventListener('dropdown-closed', listener); - const closeSpy = sandbox.spy(element, 'close'); + test('escape key', done => { + const closeSpy = sandbox.spy(element.$.dropdown, 'close'); MockInteractions.pressAndReleaseKeyOn(element, 27); flushAsynchronousOperations(); - assert.isTrue(listener.called); assert.isTrue(closeSpy.called); - assert.isTrue(element.hidden); + done(); }); test('tab key', () => { @@ -150,55 +131,4 @@ }); }); - suite('gr-autocomplete-dropdown to root', () => { - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - fixture('move').open(); - // The element was moved to the body, so look for it there. - element = Polymer.dom(document.root) - .querySelector('gr-autocomplete-dropdown'); - element.suggestions = [ - {dataValue: 'test value 1', name: 'test name 1', text: 1}, - {dataValue: 'test value 2', name: 'test name 2', text: 2}]; - }); - - teardown(() => { - sandbox.restore(); - if (!element.hidden) element.close(); - }); - - test('dropdown has been moved from the text fixture to the body', () => { - assert.equal(Polymer.dom(document.root) - .querySelectorAll('gr-autocomplete-dropdown').length, 1); - const dropdown = Polymer.dom(document.root) - .querySelector('gr-autocomplete-dropdown'); - assert.isOk(dropdown); - assert.deepEqual(dropdown.parentElement, Polymer.dom(document.root) - .querySelector('body')); - }); - - test('closing removes from body and adding adds to body', () => { - element.close(); - assert.equal(Polymer.dom(document.root) - .querySelectorAll('gr-autocomplete-dropdown').length, 0); - element.open(); - assert.equal(Polymer.dom(document.root) - .querySelectorAll('gr-autocomplete-dropdown').length, 1); - }); - - test('setPosition', () => { - const top = '10px'; - const left = '20px'; - element.setPosition(top, left); - assert.equal(getComputedStyle(element).top, top); - assert.equal(getComputedStyle(element).left, left); - }); - - test('getCurrentText', () => { - assert.equal(element.getCurrentText(), 'test value 1'); - }); - }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html index 33d3cd9..81ac90e 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -57,8 +57,7 @@ on-keydown="_handleKeydown" suggestions="[[_suggestions]]" role="listbox" - index="[[_index]]" - hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]"> + index="[[_index]]"> </gr-autocomplete-dropdown> </div> </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js index 6df6c98..aa20f96 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -147,6 +147,7 @@ observers: [ '_textChanged(text)', + '_maybeOpenDropdown(_suggestions, _focused)', ], _textChanged() { @@ -230,8 +231,11 @@ }); }, - _computeSuggestionsHidden(suggestions, focused) { - return !(suggestions.length && focused); + _maybeOpenDropdown(suggestions, focused) { + if (suggestions.length > 0 && focused) { + return this.$.suggestions.open(); + } + return this.$.suggestions.close(); }, _computeClass(borderless) { @@ -280,7 +284,7 @@ _cancel() { if (this._suggestions.length) { - this._suggestions = []; + this.set('_suggestions', []); } else { this.fire('cancel'); }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html index 038962c..fdfcddb 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -57,7 +57,7 @@ ]); }); element.query = queryStub; - assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element.$.suggestions.isHidden); assert.equal(element.$.suggestions.$.cursor.index, -1); element.text = 'blah'; @@ -66,7 +66,7 @@ element._focused = true; promise.then(() => { - assert.isFalse(element.$.suggestions.hasAttribute('hidden')); + assert.isFalse(element.$.suggestions.isHidden); const suggestions = Polymer.dom(element.$.suggestions.root).querySelectorAll('li'); assert.equal(suggestions.length, 5); @@ -89,20 +89,20 @@ }); element.query = queryStub; - assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element.$.suggestions.isHidden); element._focused = true; element.text = 'blah'; promise.then(() => { - assert.isFalse(element.$.suggestions.hidden); + assert.isFalse(element.$.suggestions.isHidden); const cancelHandler = sandbox.spy(); element.addEventListener('cancel', cancelHandler); MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); assert.isFalse(cancelHandler.called); - assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element.$.suggestions.isHidden); assert.equal(element._suggestions.length, 0); MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); @@ -124,13 +124,13 @@ }); element.query = queryStub; - assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element.$.suggestions.isHidden); assert.equal(element.$.suggestions.$.cursor.index, -1); element._focused = true; element.text = 'blah'; promise.then(() => { - assert.isFalse(element.$.suggestions.hidden); + assert.isFalse(element.$.suggestions.isHidden); const commitHandler = sandbox.spy(); element.addEventListener('commit', commitHandler); @@ -157,7 +157,7 @@ assert.equal(element.value, 1); assert.isTrue(commitHandler.called); assert.equal(commitHandler.getCall(0).args[0].detail.value, 1); - assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element.$.suggestions.isHidden); assert.isTrue(element._focused); done(); }); @@ -291,9 +291,16 @@ }); test('_focused flag shows/hides the suggestions', () => { - const suggestions = ['hello', 'its me']; - assert.isTrue(element._computeSuggestionsHidden(suggestions, false)); - assert.isFalse(element._computeSuggestionsHidden(suggestions, true)); + const openStub = sandbox.stub(element.$.suggestions, 'open'); + const closedStub = sandbox.stub(element.$.suggestions, 'close'); + element._suggestions = ['hello', 'its me']; + assert.isFalse(openStub.called); + assert.isTrue(closedStub.calledOnce); + element._focused = true; + assert.isTrue(openStub.calledOnce); + element._suggestions = []; + assert.isTrue(closedStub.calledTwice); + assert.isTrue(openStub.calledOnce); }); test('changing input sets _textChangedSinceCommit', () => { @@ -375,7 +382,7 @@ element.tabComplete = false; focusSpy = sandbox.spy(element, 'focus'); Polymer.dom.flush(); - assert.isFalse(element.$.suggestions.hidden); + assert.isFalse(element.$.suggestions.isHidden); MockInteractions.pressAndReleaseKeyOn( element.$.suggestions.$$('li:first-child'), 9, null, 'tab'); @@ -391,7 +398,7 @@ element.tabComplete = true; focusSpy = sandbox.spy(element, 'focus'); Polymer.dom.flush(); - assert.isFalse(element.$.suggestions.hidden); + assert.isFalse(element.$.suggestions.isHidden); MockInteractions.pressAndReleaseKeyOn( element.$.suggestions.$$('li:first-child'), 9, null, 'tab'); @@ -406,13 +413,13 @@ element._focused = true; element._suggestions = [{name: 'first suggestion'}]; Polymer.dom.flush(); - assert.isFalse(element.$.suggestions.hidden); + assert.isFalse(element.$.suggestions.isHidden); MockInteractions.tap(element.$.suggestions.$$('li:first-child')); flushAsynchronousOperations(); assert.isFalse(focusSpy.called); assert.isTrue(commitSpy.called); - assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element.$.suggestions.isHidden); }); });
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html index c194bcb..9be6497 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -18,122 +18,108 @@ <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../bower_components/paper-button/paper-button.html"> <link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-button"> <template strip-whitespace> <style include="shared-styles"> :host { - background-color: #f5f5f5; - border: 1px solid #d1d2d3; - border-radius: 2px; - box-sizing: border-box; - color: #333; - cursor: pointer; display: inline-block; font-family: var(--font-family-bold); font-size: 12px; - outline-width: 0; - padding: .4em .85em; position: relative; - text-align: center; - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - user-select: none; } :host([hidden]) { display: none; } - :host([primary]), - :host([secondary]) { - color: #fff; - } - :host([primary]) { - background-color: #4d90fe; - border-color: #3079ed; - } - :host([secondary]) { - background-color: #d14836; - border-color: transparent; - } - :host([small]) { - font-size: 12px; - } :host([link]) { background-color: transparent; border: none; - color: #00f; + color: var(--color-link); font-size: inherit; - font-family: var(--font-family); - padding: 0; - text-decoration: underline; + font-family: var(--font-family-bold); + text-transform: none; } - :host([loading]), - :host([disabled]) { + :host([link]) paper-button { + margin: 0; + padding: 0; + @apply --gr-button; + } + paper-button[raised] { + background-color: var(--gr-button-background, #fff); + color: var(--gr-button-color, --color-link); + } + :host([no-uppercase]) paper-button { + text-transform: none; + } + /* todo (beckysiegel) switch all secondary to primary as there is no color + distinction anymore. */ + :host([primary]) paper-button[raised], + :host([secondary]) paper-button[raised] { + background-color: var(--color-link); + color: #fff; + } + :host([primary][disabled]) paper-button[raised], + :host([disabled]) paper-button { + opacity: .5; + } + :host([link]) paper-button:hover, + :host([link]) paper-button:focus, + paper-button[raised]:hover, + paper-button[raised]:focus { + color: var(--gr-button-hover-color, --color-button-hover); + } + :host([primary]) paper-button[raised]:hover, + :host([primary]) paper-button[raised]:focus, + :host([secondary]) paper-button[raised]:hover, + :host([secondary]) paper-button[raised]:focus { + background-color: var(--gr-button-hover-background-color, --color-button-hover); + color: var(--gr-button-color, #fff); + } + paper-button, + paper-button[raised], + paper-button[link] { + display: flex; + align-items: center; + justify-content: center; + margin: 0; + min-width: 0; + padding: .4em .85em; + @apply --gr-button; + } + :host([link]) paper-button { + --paper-button: { + padding: 0; + } + } + :host:not([down-arrow]) .downArrow {display: none; } + :host([down-arrow]) .downArrow { + border-top: .36em solid var(--gr-button-arrow-color, #ccc); + border-left: .36em solid transparent; + border-right: .36em solid transparent; + margin-bottom: .05em; + margin-left: .5em; + transition: border-top-color 200ms; + } + :host([down-arrow]) paper-button:hover .downArrow { + border-top-color: var(--gr-button-arrow-hover-color, #666); + } + :host([loading]) paper-button, + :host([disabled]) paper-button { + color: #aaa; + } + :host([loading]) paper-button, + :host([loading][disabled]) paper-button { + cursor: wait; background-color: #efefef; color: #aaa; } - :host([disabled]) { - cursor: default; - } - :host([loading]), - :host([loading][disabled]) { - cursor: wait; - } - :host:focus:not([link]), - :host:hover:not([link]) { - background-color: #f8f8f8; - border-color: #aaa; - } - :host(:active) { - border-color: #d1d2d3; - color: #aaa; - } - :host([primary]:focus), - :host([secondary]:focus), - :host([primary]:active), - :host([secondary]:active) { - color: #fff; - } - :host([primary]:focus) { - box-shadow: 0 0 1px #00f; - background-color: #4d90fe; - } - :host([primary]:not([disabled]):hover) { - background-color: #4d90fe; - border-color: #00F; - } - :host([primary]:active), - :host([secondary]:active) { - box-shadow: none; - } - :host([primary]:active) { - border-color: #0c2188; - } - :host([secondary]:focus) { - box-shadow: 0 0 1px #f00; - background-color: #d14836; - } - :host([secondary]:not([disabled]):hover) { - background-color: #c53727; - border: 1px solid #b0281a; - } - :host([secondary]:active) { - border-color: #941c0c; - } - :host([primary][loading]) { - background-color: #7caeff; - border-color: transparent; - color: #fff; - } - :host([primary][disabled]) { - background-color: #4d90fe; - color: #fff; - opacity: .5; - } </style> - <content></content> + <paper-button raised="[[!link]]" disabled="[[disabled]]" tabindex="-1"> + <content></content> + <i class="downArrow"></i> + </paper-button> </template> <script src="gr-button.js"></script> -</dom-module> +</dom-module> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js index ddb2bc3..551fd7f 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -18,8 +18,13 @@ is: 'gr-button', properties: { + downArrow: { + type: Boolean, + reflectToAttribute: true, + }, link: { type: Boolean, + value: false, reflectToAttribute: true, }, disabled: { @@ -27,6 +32,10 @@ observer: '_disabledChanged', reflectToAttribute: true, }, + noUppercase: { + type: Boolean, + value: false, + }, _enabledTabindex: { type: String, value: '0',
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html index 59f63fa..91c2def 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -37,12 +37,6 @@ -webkit-user-select: text; user-select: text; } - .downArrow { - display: inline-block; - font-size: .6em; - user-select: none; - vertical-align: middle; - } .dropdown-trigger { cursor: pointer; padding: 0; @@ -52,7 +46,8 @@ box-shadow: 0 1px 5px rgba(0, 0, 0, .3); max-height: 70vh; margin-top: 1.5em; - width: 266px; + min-width: 266px; + max-width: 300px; } paper-listbox { --paper-listbox: { @@ -62,6 +57,7 @@ paper-item { cursor: pointer; flex-direction: column; + font-size: 1em; --paper-item: { min-height: 0; padding: 10px 16px; @@ -76,18 +72,19 @@ background-color: #f2f2f2; } } + gr-button { + --gr-button-arrow-color: var(--color-link); + --gr-button-arrow-hover-color: var(--color-link-hover); + } paper-item:not(:last-of-type) { border-bottom: 1px solid #ddd; } - gr-button { - color: black; - font: inherit; + #trigger { padding: .3em 0; - text-decoration: none; } .bottomContent { color: rgba(0,0,0,.54); - font-size: .85em; + font-size: .9em; line-height: 16px; } .bottomContent, @@ -105,6 +102,16 @@ gr-select { display: none; } + /* Because the iron dropdown 'area' includes the trigger, and the entire + width of the dropdown, we want to treat tapping the area above the + dropdown content as if it is tapping whatever content is underneath it. + The next two styles allow this to happen. */ + iron-dropdown { + pointer-events: none; + } + paper-listbox { + pointer-events: auto; + } @media only screen and (max-width: 50em) { gr-select { display: inline; @@ -119,25 +126,25 @@ } </style> <gr-button + down-arrow link id="trigger" class="dropdown-trigger" - on-tap="_showDropdownTapHandler"> + on-tap="_showDropdownTapHandler" + slot="dropdown-trigger"> <span>[[text]]</span> - <span - class="downArrow" - on-tap="_showDropdownTapHandler">▼</span> </gr-button> <iron-dropdown id="dropdown" vertical-align="top" - allow-outside-scroll="true"> + allow-outside-scroll="true" + on-tap="_handleDropdownTap"> <paper-listbox class="dropdown-content" slot="dropdown-content" attr-for-selected="value" - on-tap="_handleDropdownTap" - selected="{{value}}"> + selected="{{value}}" + on-tap="_handleDropdownTap"> <template is="dom-repeat" items="[[items]]"> <paper-item disabled="[[item.disabled]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js index 27c6ba8..dc8b44e 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -95,6 +95,7 @@ const selectedObj = items.find(item => { return item.value + '' === value + ''; }); + if (!selectedObj) { return; } this.text = selectedObj.triggerText? selectedObj.triggerText : selectedObj.text; },
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html index f4813fa..b821909 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -43,9 +43,6 @@ font: inherit; padding: .3em 0; } - :host([down-arrow]) .dropdown-trigger { - padding-right: 1.4em; - } gr-avatar { height: 2em; width: 2em; @@ -75,7 +72,6 @@ } li .itemAction:link, li .itemAction:visited { - color: #00e; text-decoration: none; } li .itemAction:not(.disabled):hover { @@ -94,26 +90,13 @@ .bold-text { font-family: var(--font-family-bold); } - :host:not([down-arrow]) .downArrow { display: none; } - :host([down-arrow]) .downArrow { - border-left: .36em solid transparent; - border-right: .36em solid transparent; - border-top: .36em solid #ccc; - height: 0; - position: absolute; - right: .3em; - top: calc(50% - .05em); - transition: border-top-color 200ms; - width: 0; - } - .dropdown-trigger:hover .downArrow { - border-top-color: #666; - } </style> - <gr-button link="[[link]]" class="dropdown-trigger" id="trigger" + <gr-button + link="[[link]]" + class="dropdown-trigger" id="trigger" + down-arrow="[[downArrow]]" on-tap="_showDropdownTapHandler"> <content></content> - <i class="downArrow"></i> </gr-button> <iron-dropdown id="dropdown" vertical-align="top"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js index ec8f04c..49e5a5e 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -37,6 +37,7 @@ type: Array, observer: '_resetCursorStops', }, + downArrow: Boolean, topContent: Object, horizontalAlign: { type: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html index 79d69f5..6eb7c8d 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -27,6 +27,9 @@ align-items: center; display: inline-flex; } + :host([uppercase]) label { + text-transform: uppercase; + } input, label { width: 100%; @@ -37,14 +40,14 @@ label { color: #777; display: inline-block; + font-family: var(--font-family-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } label.editable { - color: #00f; + color: var(--color-link); cursor: pointer; - text-decoration: underline; } #dropdown { box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js index f87a546..b0e8516 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -46,6 +46,11 @@ type: Boolean, value: false, }, + uppercase: { + type: Boolean, + reflectToAttribute: true, + value: false, + }, _inputText: String, // This is used to push the iron-input element up on the page, so // the input is placed in approximately the same position as the
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html index be71eb6..d30bad2 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -34,24 +34,31 @@ display: inline-flex; padding: 0 .5em; } - gr-button.remove, gr-button.remove:hover, gr-button.remove:focus { - border-color: transparent; - color: #333; + --gr-button: { + color: #333; + } } gr-button.remove { - background: #eee; - border: 0; - color: #666; - font-size: 1.7em; - font-weight: normal; - height: .6em; - line-height: .6em; - margin-left: .15em; - margin-top: -.05em; - padding: 0; - text-decoration: none; + --gr-button: { + border: 0; + color: #666; + font-size: 1.7em; + font-weight: normal; + height: .6em; + line-height: .6em; + margin-left: .15em; + margin-top: -.05em; + padding: 0; + text-decoration: none; + } + --gr-button-hover-color: { + color: #333; + } + --gr-button-hover-background-color: { + color: #333; + } } .transparentBackground, gr-button.transparentBackground { @@ -68,6 +75,7 @@ </a> <gr-button id="remove" + link hidden$="[[!removable]]" hidden class$="remove [[_getBackgroundClass(transparentBackground)]]"
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 4e0f3b7..5153fb0 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
@@ -486,6 +486,14 @@ }); }, + /** + * @param {string} userId the ID of the user usch as an email address. + * @return {!Promise<!Object>} + */ + getAccountDetails(userId) { + return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/detail`); + }, + getAccountEmails() { return this._fetchSharedCacheURL('/accounts/self/emails'); }, @@ -579,6 +587,10 @@ }); }, + getAccountStatus(userId) { + return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/status`); + }, + getAccountGroups() { return this._fetchSharedCacheURL('/accounts/self/groups'); },
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html index 59daf07..95df7c9 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -28,10 +28,14 @@ <style include="shared-styles"> :host { display: block; + position: relative; } :host(.monospace) { font-family: var(--monospace-font-family); } + #emojiSuggestions { + font-family: var(--font-family); + } gr-autocomplete { display: inline-block } @@ -39,11 +43,16 @@ background-color: var(--background-color, none); width: 100%; } + #hiddenText #emojiSuggestions { + visibility: visible; + white-space: normal; + } /*This is needed to not add a scroll bar on the side of gr-textarea since there is 2px of padding in iron-autogrow-textarea for the native textarea*/ iron-autogrow-textarea { padding: 2px; + position: relative; } #textarea.noBorder { border: none; @@ -53,17 +62,19 @@ float: left; position: absolute; visibility: hidden; - white-space: pre-wrap + width: 100%; + white-space: pre-wrap; } </style> - <gr-autocomplete-dropdown id="emojiSuggestions" + <div id="hiddenText"></div> + <gr-autocomplete-dropdown + id="emojiSuggestions" suggestions="[[_suggestions]]" index="[[_index]]" - move-to-root - fixed-position="[[fixedPositionDropdown]]" - hidden> + vertical-offset="[[_verticalOffset]]" + on-dropdown-closed="_resetAndFocus" + on-item-selected="_handleEmojiSelect"> </gr-autocomplete-dropdown> - <div id="hiddenText"></div> <iron-autogrow-textarea id="textarea" autocomplete="[[autocomplete]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js index a172a09..2d796bd 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -15,7 +15,6 @@ 'use strict'; const MAX_ITEMS_DROPDOWN = 10; - const VERTICAL_OFFSET = 7; const ALL_SUGGESTIONS = [ {value: '💯', match: '100'}, @@ -63,8 +62,6 @@ rows: Number, maxRows: Number, placeholder: String, - fixedPositionDropdown: Boolean, - moveToRoot: Boolean, text: { type: String, notify: true, @@ -95,6 +92,12 @@ }, _index: Number, _suggestions: Array, + // Offset makes dropdown appear below text. + _verticalOffset: { + type: Number, + value: 20, + readOnly: true, + }, }, behaviors: [ @@ -120,22 +123,11 @@ if (this.backgroundColor) { this.updateStyles({'--background-color': this.backgroundColor}); } - this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus'); - this.listen(this.$.emojiSuggestions, 'item-selected', - '_handleEmojiSelect'); }, - detached() { - this.closeDropdown(); - this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus'); - this.listen(this.$.emojiSuggestions, 'item-selected', - '_handleEmojiSelect'); - }, closeDropdown() { - if (!this.$.emojiSuggestions.hidden) { - this._closeEmojiDropdown(); - } + return this.$.emojiSuggestions.close(); }, getNativeTextarea() { @@ -161,7 +153,6 @@ _resetAndFocus() { this._resetEmojiDropdown(); - this.$.textarea.textarea.focus(); }, _handleUpKey(e) { @@ -197,14 +188,17 @@ return this.text.substr(0, this._colonIndex || 0) + value + this.text.substr(this.$.textarea.selectionStart) + ' '; }, - - _getPositionOfCursor() { + /** + * Uses a hidden element with the same width and styling of the textarea and + * the text up until the point of interest. Then the emoji selection + * element is added to the end so that they are correctly positioned by the + * end of the last character entered. + */ + _updateCaratPosition() { this.$.hiddenText.textContent = this.$.textarea.value.substr(0, this.$.textarea.selectionStart); - const caratSpan = document.createElement('span'); - this.$.hiddenText.appendChild(caratSpan); - return caratSpan.getBoundingClientRect(); + this.$.hiddenText.appendChild(this.$.emojiSuggestions); }, _getFontSize() { @@ -218,29 +212,6 @@ }, /** - * This positions the dropdown to be just below the cursor position. It is - * calculated by having a hidden element with the same width and styling of - * the tetarea and the text up until the point of interest. Then a span - * element is added to the end so that there is a specific element to get - * the position of. Line height is determined (or falls back to 12px) as - * extra height to add. - */ - _updateSelectorPosition() { - // These are broken out into separate functions for testability. - const caratPosition = this._getPositionOfCursor(); - const fontSize = this._getFontSize(); - - let top = caratPosition.top + fontSize + VERTICAL_OFFSET; - - if (!this.fixedPositionDropdown) { - top += this._getScrollTop(); - } - top += 'px'; - const left = caratPosition.left + 'px'; - this.$.emojiSuggestions.setPosition(top, left); - }, - - /** * _handleKeydown used for key handling in the this.$.textarea AND all child * autocomplete options. */ @@ -278,23 +249,16 @@ this._resetEmojiDropdown(); // Otherwise open the dropdown and set the position to be just below the // cursor. - } else if (this.$.emojiSuggestions.hidden) { + } else if (this.$.emojiSuggestions.isHidden) { this._hideAutocomplete = false; this._openEmojiDropdown(); - this._updateSelectorPosition(); + this._updateCaratPosition(); } this.$.textarea.textarea.focus(); } }, - - _closeEmojiDropdown() { - this.$.emojiSuggestions.close(); - this.$.emojiSuggestions.hidden = true; - }, - _openEmojiDropdown() { this.$.emojiSuggestions.open(); - this.$.emojiSuggestions.hidden = false; }, _formatSuggestions(matchedSuggestions) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html index 95e3a8d..493dd5d 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -72,7 +72,7 @@ element.$.textarea.selectionStart = 1; element.$.textarea.selectionEnd = 1; element.text = ':'; - assert.isFalse(!element.$.emojiSuggestions.hidden); + assert.isFalse(!element.$.emojiSuggestions.isHidden); }); test('emoji selector is not open when a general text is entered', () => { @@ -80,7 +80,7 @@ element.$.textarea.selectionStart = 9; element.$.textarea.selectionEnd = 9; element.text = 'some text'; - assert.isFalse(!element.$.emojiSuggestions.hidden); + assert.isFalse(!element.$.emojiSuggestions.isHidden); }); test('emoji selector opens when a colon is typed & the textarea has focus', @@ -95,7 +95,7 @@ element.$.textarea.selectionEnd = 2; element.text = ':t'; flushAsynchronousOperations(); - assert.isFalse(element.$.emojiSuggestions.hidden); + assert.isFalse(element.$.emojiSuggestions.isHidden); assert.equal(element._colonIndex, 0); assert.isFalse(element._hideAutocomplete); assert.equal(element._currentSearchString, 't'); @@ -165,32 +165,16 @@ assert.equal(element.text, 'test test 😂 '); }); - test('_getPositionOfCursor', () => { + test('_updateCaratPosition', () => { element.$.textarea.selectionStart = 4; element.$.textarea.selectionEnd = 4; element.text = 'test'; - element._getPositionOfCursor(); + element._updateCaratPosition(); assert.deepEqual(element.$.hiddenText.innerHTML, element.text + - '<span></span>'); + element.$.emojiSuggestions.outerHTML); }); - test('_updateSelectorPosition', () => { - const setPositionSpy = - sandbox.spy(element.$.emojiSuggestions, 'setPosition'); - sandbox.stub(element, '_getPositionOfCursor', () => { - return {top: 100, left: 30}; - }); - sandbox.stub(element, '_getFontSize', () => 12); - sandbox.stub(element, '_getScrollTop', () => 100); - element._updateSelectorPosition(); - assert.isTrue(setPositionSpy.lastCall.calledWithExactly('219px', '30px')); - - element.fixedPositionDropdown = true; - element._updateSelectorPosition(); - assert.isTrue(setPositionSpy.lastCall.calledWithExactly('119px', '30px')); - }); - - test('emoji dropdown is closed when dropdown-closed is fired', () => { + test('emoji dropdown is closed when iron-overlay-closed is fired', () => { const resetSpy = sandbox.spy(element, '_resetAndFocus'); element.$.emojiSuggestions.fire('dropdown-closed'); assert.isTrue(resetSpy.called); @@ -205,56 +189,67 @@ }); suite('keyboard shortcuts', () => { - function setupDropdown() { - MockInteractions.focus(element.$.textarea); + function setupDropdown(callback) { + element.$.emojiSuggestions.addEventListener('open-complete', () => { + callback(); + }); flushAsynchronousOperations(); + MockInteractions.focus(element.$.textarea); element.$.textarea.selectionStart = 1; element.$.textarea.selectionEnd = 1; element.text = ':'; element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; + element.$.textarea.selectionEnd = 2; element.text = ':1'; } - test('escape key', () => { + test('escape key', done => { const resestSpy = sandbox.spy(element, '_resetAndFocus'); MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); assert.isFalse(resestSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); - assert.isTrue(resestSpy.called); - assert.isFalse(!element.$.emojiSuggestions.hidden); + setupDropdown(() => { + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); + assert.isTrue(resestSpy.called); + assert.isFalse(!element.$.emojiSuggestions.isHidden); + done(); + }); }); - test('up key', () => { + test('up key', done => { const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp'); MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); assert.isFalse(upSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); - assert.isTrue(upSpy.called); + setupDropdown(() => { + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); + assert.isTrue(upSpy.called); + done(); + }); }); - test('down key', () => { + test('down key', done => { const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown'); MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); assert.isFalse(downSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); - assert.isTrue(downSpy.called); + setupDropdown(() => { + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); + assert.isTrue(downSpy.called); + done(); + }); }); - test('enter key', () => { + test('enter key', done => { const enterSpy = sandbox.spy(element.$.emojiSuggestions, 'getCursorTarget'); MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); assert.isFalse(enterSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); - assert.isTrue(enterSpy.called); - flushAsynchronousOperations(); - // A space is automatically added at the end. - assert.equal(element.text, '💯 '); + setupDropdown(() => { + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isTrue(enterSpy.called); + flushAsynchronousOperations(); + // A space is automatically added at the end. + assert.equal(element.text, '💯 '); + done(); + }); }); }); });
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html index 4318757..6a81158 100644 --- a/polygerrit-ui/app/styles/app-theme.html +++ b/polygerrit-ui/app/styles/app-theme.html
@@ -35,6 +35,11 @@ --iron-overlay-backdrop: { transition: none; } + + /* Follow are a part of the design refresh */ + --color-link: #2a66d9; + /* 12% darker */ + --color-button-hover: #0B47BA; } @media screen and (max-width: 50em) { :root {
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html index 5c11d60..7389fa4 100644 --- a/polygerrit-ui/app/styles/shared-styles.html +++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -38,6 +38,9 @@ margin: 0; padding: 0; } + a { + color: var(--color-link); + } input, textarea, select,
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index c748a9b..7080eb7 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -52,6 +52,7 @@ 'change-list/gr-change-list-item/gr-change-list-item_test.html', 'change-list/gr-change-list-view/gr-change-list-view_test.html', 'change-list/gr-change-list/gr-change-list_test.html', + 'change-list/gr-user-header/gr-user-header_test.html', 'change/gr-account-entry/gr-account-entry_test.html', 'change/gr-account-list/gr-account-list_test.html', 'change/gr-change-actions/gr-change-actions_test.html', @@ -160,6 +161,7 @@ // Behaviors tests. const behaviors = [ + 'async-foreach-behavior/async-foreach-behavior_test.html', 'base-url-behavior/base-url-behavior_test.html', 'docs-url-behavior/docs-url-behavior_test.html', 'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py index 2426b9f..f7b5aa8 100755 --- a/tools/maven/mvn.py +++ b/tools/maven/mvn.py
@@ -19,10 +19,6 @@ from subprocess import check_output from sys import stderr - -def mvn(): - return ['mvn', '--file', path.join(root, 'fake_pom.xml'), '-DgroupId=com.google.gerrit'] - opts = OptionParser() opts.add_option('--repository', help='maven repository id') opts.add_option('--url', help='maven repository url') @@ -41,12 +37,14 @@ root = path.dirname(root) if 'install' == args.a: - cmd = mvn() + [ + cmd = [ + 'mvn', 'install:install-file', '-Dversion=%s' % args.v, ] elif 'deploy' == args.a: - cmd = mvn() + [ + cmd = [ + 'mvn', 'gpg:sign-and-deploy-file', '-DrepositoryId=%s' % args.repository, '-Durl=%s' % args.url, @@ -58,7 +56,7 @@ for spec in args.s: artifact, packaging_type, src = spec.split(':') exe = cmd + [ - '-DartifactId=%s' % artifact, + '-DpomFile=%s' % path.join(root, '%s/pom.xml' % artifact), '-Dpackaging=%s' % packaging_type, '-Dfile=%s' % src, ]
diff --git a/tools/version.py b/tools/version.py new file mode 100755 index 0000000..fed6d5d --- /dev/null +++ b/tools/version.py
@@ -0,0 +1,55 @@ +#!/usr/bin/env python +# Copyright (C) 2014 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. + +from __future__ import print_function +from optparse import OptionParser +import os.path +import re +import sys + +parser = OptionParser() +opts, args = parser.parse_args() + +if not len(args): + parser.error('not enough arguments') +elif len(args) > 1: + parser.error('too many arguments') + +DEST_PATTERN = r'\g<1>%s\g<3>' % args[0] + + +def replace_in_file(filename, src_pattern): + try: + f = open(filename, "r") + s = f.read() + f.close() + s = re.sub(src_pattern, DEST_PATTERN, s) + f = open(filename, "w") + f.write(s) + f.close() + except IOError as err: + print('error updating %s: %s' % (filename, err), file=sys.stderr) + + +src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$', + re.MULTILINE) +for project in ['gerrit-acceptance-framework', 'gerrit-extension-api', + 'gerrit-plugin-api', 'gerrit-plugin-gwtui', + 'gerrit-war']: + pom = os.path.join(project, 'pom.xml') + replace_in_file(pom, src_pattern) + +src_pattern = re.compile(r'^(GERRIT_VERSION = ")([-.\w]+)(")$', re.MULTILINE) +replace_in_file('version.bzl', src_pattern)
diff --git a/version.bzl b/version.bzl index 488a83f..62d841f 100644 --- a/version.bzl +++ b/version.bzl
@@ -2,4 +2,4 @@ # Used by :api_install and :api_deploy targets # when talking to the destination repository. # -GERRIT_VERSION = "2.15-SNAPSHOT" +GERRIT_VERSION = "2.16-SNAPSHOT"