Merge "Move capability evaluation to DefaultPermissionBackend"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 7440634..8cddce2 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -111,6 +111,10 @@
* `reviewer_suggestion/query_groups`: Latency for querying groups for reviewer
suggestion.
+=== Repo Sequences
+
+* `sequence/next_id_latency`: Latency of requesting IDs from repo sequences.
+
=== Replication Plugin
* `plugins/replication/replication_latency`: Time spent pushing to remote
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 16e61f6..d64054a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2121,7 +2121,7 @@
----
[[set-work-in-pogress]]
-== Set Work-In-Progress
+=== Set Work-In-Progress
--
'POST /changes/link:#change-id[\{change-id\}]/wip'
--
@@ -2149,7 +2149,7 @@
----
[[set-ready-for-review]]
-== Set Ready-For-Review
+=== Set Ready-For-Review
--
'POST /changes/link:#change-id[\{change-id\}]/ready'
--
@@ -2807,6 +2807,8 @@
Suggest the reviewers for a given query `q` and result limit `n`. If result
limit is not passed, then the default 10 is used.
+Groups can be excluded from the results by specifying 'e=f'.
+
As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned.
.Request
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index cb72519b..434a7a4 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -1130,7 +1130,7 @@
- [[line-wrapping]]`Line Wrapping`:
+
-Controls weather to enable line wrapping or not.
+Controls whether to enable line wrapping or not.
+
If `false` is selected then line wrapping is disabled.
This is the default option.
diff --git a/WORKSPACE b/WORKSPACE
index 63d10c6..2db3a6c 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -883,13 +883,13 @@
maven_jar(
name = "codemirror_minified",
artifact = "org.webjars.npm:codemirror-minified:" + CM_VERSION,
- sha1 = "a62e6dc43acc09df962edeb8013c987da7739e7a",
+ sha1 = "7464d1bf59a36b081981b855a51839bf3a1f302d",
)
maven_jar(
name = "codemirror_original",
artifact = "org.webjars.npm:codemirror:" + CM_VERSION,
- sha1 = "7a5ae457aa3bc0023d21bc75793bac86b83b0c51",
+ sha1 = "8bcf4d541eba5c1c6916cb449fe4baf73d2ebd6f",
)
maven_jar(
@@ -1101,6 +1101,13 @@
)
bower_archive(
+ name = "polymer-resin",
+ package = "polymer/polymer-resin",
+ sha1 = "d759c8c09054a7ec04608a6cb586801c904f79a2",
+ version = "1.2.6-beta",
+)
+
+bower_archive(
name = "promise-polyfill",
package = "polymerlabs/promise-polyfill",
sha1 = "a3b598c06cbd7f441402e666ff748326030905d6",
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index 87105f8..918b7a6 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -19,7 +19,6 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
@@ -109,10 +108,15 @@
}
externalIdsUpdate.create().insert(extIds);
- Account a = new Account(id, TimeUtil.nowTs());
- a.setFullName(fullName);
- a.setPreferredEmail(email);
- accountsUpdate.create().insert(db, a);
+ accountsUpdate
+ .create()
+ .insert(
+ db,
+ id,
+ a -> {
+ a.setFullName(fullName);
+ a.setPreferredEmail(email);
+ });
if (groups != null) {
for (String n : groups) {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 2b13df0..72d646e 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -46,6 +46,7 @@
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
@@ -54,6 +55,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.eclipse.jgit.lib.Config;
@@ -233,7 +235,7 @@
if (!desc.memory()) {
init(desc, baseConfig, site);
}
- return start(desc, baseConfig, site);
+ return start(desc, baseConfig, site, null);
}
/**
@@ -244,10 +246,18 @@
* @param site existing temporary directory for site. Required, but may be empty, for in-memory
* servers. For on-disk servers, assumes that {@link #init} was previously called to
* initialize this directory.
+ * @param testSysModule optional additional module to add to the system injector.
+ * @param additionalArgs additional command-line arguments for the daemon program; only allowed if
+ * the test is not in-memory.
* @return started server.
* @throws Exception
*/
- public static GerritServer start(Description desc, Config baseConfig, Path site)
+ public static GerritServer start(
+ Description desc,
+ Config baseConfig,
+ Path site,
+ @Nullable Module testSysModule,
+ String... additionalArgs)
throws Exception {
checkArgument(site != null, "site is required (even for in-memory server");
desc.checkValidAnnotations();
@@ -264,12 +274,14 @@
},
site);
daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
+ daemon.setAdditionalSysModuleForTesting(testSysModule);
daemon.setEnableSshd(desc.useSsh());
if (desc.memory()) {
+ checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
return startInMemory(desc, baseConfig, daemon);
}
- return startOnDisk(desc, site, daemon, serverStarted);
+ return startOnDisk(desc, site, daemon, serverStarted, additionalArgs);
}
private static GerritServer startInMemory(Description desc, Config baseConfig, Daemon daemon)
@@ -291,18 +303,25 @@
}
private static GerritServer startOnDisk(
- Description desc, Path site, Daemon daemon, CyclicBarrier serverStarted) throws Exception {
+ Description desc,
+ Path site,
+ Daemon daemon,
+ CyclicBarrier serverStarted,
+ String[] additionalArgs)
+ throws Exception {
checkNotNull(site);
ExecutorService daemonService = Executors.newSingleThreadExecutor();
+ String[] args =
+ Stream.concat(
+ Stream.of(
+ "-d", site.toString(), "--headless", "--console-log", "--show-stack-trace"),
+ Arrays.stream(additionalArgs))
+ .toArray(String[]::new);
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
daemonService.submit(
() -> {
- int rc =
- daemon.main(
- new String[] {
- "-d", site.toString(), "--headless", "--console-log", "--show-stack-trace",
- });
+ int rc = daemon.main(args);
if (rc != 0) {
System.err.println("Failed to start Gerrit daemon");
serverStarted.reset();
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index be34ba6..93273c4 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.fail;
import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -29,6 +30,7 @@
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.inject.Injector;
+import com.google.inject.Module;
import com.google.inject.Provider;
import java.util.Arrays;
import org.eclipse.jgit.lib.Config;
@@ -113,19 +115,26 @@
}
protected ServerContext startServer() throws Exception {
- return new ServerContext(startImpl());
+ return startServer(null);
+ }
+
+ protected ServerContext startServer(@Nullable Module testSysModule, String... additionalArgs)
+ throws Exception {
+ return new ServerContext(startImpl(testSysModule, additionalArgs));
}
protected void assertServerStartupFails() throws Exception {
- try (GerritServer server = startImpl()) {
+ try (GerritServer server = startImpl(null)) {
fail("expected server startup to fail");
} catch (GerritServer.StartupException e) {
// Expected.
}
}
- private GerritServer startImpl() throws Exception {
- return GerritServer.start(serverDesc, baseConfig, sitePaths.site_path);
+ private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
+ throws Exception {
+ return GerritServer.start(
+ serverDesc, baseConfig, sitePaths.site_path, testSysModule, additionalArgs);
}
protected static void runGerrit(String... args) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 5fbf8ca..50bc0be 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -32,6 +32,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.fail;
import com.google.common.collect.FluentIterable;
@@ -74,6 +75,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountConfig;
import com.google.gerrit.server.account.WatchConfig;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -91,7 +93,6 @@
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
@@ -106,6 +107,7 @@
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
@@ -114,6 +116,7 @@
import org.eclipse.jgit.transport.PushCertificateIdent;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -162,8 +165,8 @@
externalIdsUpdate = externalIdsUpdateFactory.create();
savedExternalIds = new ArrayList<>();
- savedExternalIds.addAll(getExternalIds(admin));
- savedExternalIds.addAll(getExternalIds(user));
+ savedExternalIds.addAll(externalIds.byAccount(admin.id));
+ savedExternalIds.addAll(externalIds.byAccount(user.id));
}
@After
@@ -172,8 +175,8 @@
// savedExternalIds is null when we don't run SSH tests and the assume in
// @Before in AbstractDaemonTest prevents this class' @Before method from
// being executed.
- externalIdsUpdate.delete(getExternalIds(admin));
- externalIdsUpdate.delete(getExternalIds(user));
+ externalIdsUpdate.delete(externalIds.byAccount(admin.id));
+ externalIdsUpdate.delete(externalIds.byAccount(user.id));
externalIdsUpdate.insert(savedExternalIds);
}
}
@@ -190,10 +193,6 @@
}
}
- private Collection<ExternalId> getExternalIds(TestAccount account) throws Exception {
- return accountCache.get(account.getId()).getExternalIds();
- }
-
@After
public void deleteGpgKeys() throws Exception {
String ref = REFS_GPG_KEYS;
@@ -220,6 +219,67 @@
create(3); // account creation + external ID creation + adding SSH keys
}
+ private void create(int expectedAccountReindexCalls) throws Exception {
+ String name = "foo";
+ TestAccount foo = accountCreator.create(name);
+ AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+ assertThat(info.username).isEqualTo(name);
+ assertThat(info.name).isEqualTo(name);
+ accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
+
+ // check user branch
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo);
+ ObjectReader or = repo.newObjectReader()) {
+ Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
+ assertThat(ref).isNotNull();
+ RevCommit c = rw.parseCommit(ref.getObjectId());
+ long timestampDiffMs =
+ Math.abs(
+ c.getCommitTime() * 1000L
+ - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
+ assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+
+ // Check the 'account.config' file.
+ try (TreeWalk tw = TreeWalk.forPath(or, AccountConfig.ACCOUNT_CONFIG, c.getTree())) {
+ assertThat(tw).isNotNull();
+ Config cfg = new Config();
+ cfg.fromText(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8));
+ assertThat(cfg.getString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_FULL_NAME))
+ .isEqualTo(name);
+ }
+ }
+ }
+
+ @Test
+ public void createAnonymousCoward() throws Exception {
+ TestAccount anonymousCoward = accountCreator.create();
+ accountIndexedCounter.assertReindexOf(anonymousCoward);
+
+ // check user branch
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo);
+ ObjectReader or = repo.newObjectReader()) {
+ Ref ref = repo.exactRef(RefNames.refsUsers(anonymousCoward.getId()));
+ assertThat(ref).isNotNull();
+ RevCommit c = rw.parseCommit(ref.getObjectId());
+ long timestampDiffMs =
+ Math.abs(
+ c.getCommitTime() * 1000L
+ - accountCache
+ .get(anonymousCoward.getId())
+ .getAccount()
+ .getRegisteredOn()
+ .getTime());
+ assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+
+ // No account properties were set, hence an 'account.config' file was not created.
+ try (TreeWalk tw = TreeWalk.forPath(or, AccountConfig.ACCOUNT_CONFIG, c.getTree())) {
+ assertThat(tw).isNull();
+ }
+ }
+ }
+
@Test
public void get() throws Exception {
AccountInfo info = gApi.accounts().id("admin").get();
@@ -694,6 +754,36 @@
}
@Test
+ public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmit() throws Exception {
+ String userRefName = RefNames.refsUsers(admin.id);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ fetch(allUsersRepo, userRefName + ":userRef");
+ allUsersRepo.reset("userRef");
+
+ Config ac = getAccountConfig(allUsersRepo);
+ ac.setString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_STATUS, "OOO");
+
+ PushOneCommit.Result r =
+ pushFactory
+ .create(
+ db,
+ admin.getIdent(),
+ allUsersRepo,
+ "Update account config",
+ AccountConfig.ACCOUNT_CONFIG,
+ ac.toText())
+ .to(MagicBranch.NEW_CHANGE + userRefName);
+ r.assertOkStatus();
+ accountIndexedCounter.assertNoReindex();
+ assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage(
+ String.format("update of %s not allowed", AccountConfig.ACCOUNT_CONFIG));
+ gApi.changes().id(r.getChangeId()).current().submit();
+ }
+
+ @Test
public void pushWatchConfigToUserBranch() throws Exception {
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
@@ -735,6 +825,28 @@
}
@Test
+ public void pushAccountConfigToUserBranchIsRejected() throws Exception {
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
+ allUsersRepo.reset("userRef");
+
+ Config ac = getAccountConfig(allUsersRepo);
+ ac.setString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_STATUS, "OOO");
+
+ PushOneCommit.Result r =
+ pushFactory
+ .create(
+ db,
+ admin.getIdent(),
+ allUsersRepo,
+ "Update account config",
+ AccountConfig.ACCOUNT_CONFIG,
+ ac.toText())
+ .to(RefNames.REFS_USERS_SELF);
+ r.assertErrorStatus("account update not allowed");
+ }
+
+ @Test
@Sandboxed
public void cannotCreateUserBranch() throws Exception {
grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
@@ -1071,26 +1183,6 @@
assertThat(checkInfo.checkAccountsResult.problems).containsExactlyElementsIn(expectedProblems);
}
- public void create(int expectedAccountReindexCalls) throws Exception {
- TestAccount foo = accountCreator.create("foo");
- AccountInfo info = gApi.accounts().id(foo.id.get()).get();
- assertThat(info.username).isEqualTo("foo");
- accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
-
- // check user branch
- try (Repository repo = repoManager.openRepository(allUsers);
- RevWalk rw = new RevWalk(repo)) {
- Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
- assertThat(ref).isNotNull();
- RevCommit c = rw.parseCommit(ref.getObjectId());
- long timestampDiffMs =
- Math.abs(
- c.getCommitTime() * 1000L
- - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
- assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
- }
- }
-
private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
int seq = 1;
for (SshKeyInfo key : sshKeys) {
@@ -1212,6 +1304,26 @@
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
}
+ private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
+ Config ac = new Config();
+ try (TreeWalk tw =
+ TreeWalk.forPath(
+ allUsersRepo.getRepository(),
+ AccountConfig.ACCOUNT_CONFIG,
+ getHead(allUsersRepo.getRepository()).getTree())) {
+ assertThat(tw).isNotNull();
+ ac.fromText(
+ new String(
+ allUsersRepo
+ .getRevWalk()
+ .getObjectReader()
+ .open(tw.getObjectId(0), OBJ_BLOB)
+ .getBytes(),
+ UTF_8));
+ }
+ return ac;
+ }
+
private static class AccountIndexedCounter implements AccountIndexedListener {
private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
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 8607c02..275b06a 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
@@ -2926,6 +2926,7 @@
RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+ assertThat(rApi.description()).isEqualTo("Edit commit message");
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
index f405e19..3f45711c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
@@ -4,4 +4,12 @@
srcs = glob(["*IT.java"]),
group = "pgm",
labels = ["pgm"],
+ deps = [":util"],
+)
+
+java_library(
+ name = "util",
+ testonly = 1,
+ srcs = ["IndexUpgradeController.java"],
+ deps = ["//gerrit-acceptance-tests:lib"],
)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
new file mode 100644
index 0000000..9cdcb40
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.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.acceptance.pgm;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+class IndexUpgradeController implements OnlineUpgradeListener {
+ @AutoValue
+ abstract static class UpgradeAttempt {
+ static UpgradeAttempt create(String name, int oldVersion, int newVersion) {
+ return new AutoValue_IndexUpgradeController_UpgradeAttempt(name, oldVersion, newVersion);
+ }
+
+ abstract String name();
+
+ abstract int oldVersion();
+
+ abstract int newVersion();
+ }
+
+ private final int numExpected;
+ private final CountDownLatch readyToStart;
+ private final CountDownLatch started;
+ private final CountDownLatch finished;
+
+ private final List<UpgradeAttempt> startedAttempts;
+ private final List<UpgradeAttempt> succeededAttempts;
+ private final List<UpgradeAttempt> failedAttempts;
+
+ IndexUpgradeController(int numExpected) {
+ this.numExpected = numExpected;
+ readyToStart = new CountDownLatch(1);
+ started = new CountDownLatch(numExpected);
+ finished = new CountDownLatch(numExpected);
+ startedAttempts = new ArrayList<>();
+ succeededAttempts = new ArrayList<>();
+ failedAttempts = new ArrayList<>();
+ }
+
+ Module module() {
+ return new AbstractModule() {
+ @Override
+ public void configure() {
+ DynamicSet.bind(binder(), OnlineUpgradeListener.class)
+ .toInstance(IndexUpgradeController.this);
+ }
+ };
+ }
+
+ @Override
+ public synchronized void onStart(String name, int oldVersion, int newVersion) {
+ UpgradeAttempt a = UpgradeAttempt.create(name, oldVersion, newVersion);
+ try {
+ readyToStart.await();
+ } catch (InterruptedException e) {
+ throw new AssertionError("interrupted waiting to start " + a, e);
+ }
+ checkState(
+ started.getCount() > 0, "already started %s upgrades, can't start %s", numExpected, a);
+ startedAttempts.add(a);
+ started.countDown();
+ }
+
+ @Override
+ public synchronized void onSuccess(String name, int oldVersion, int newVersion) {
+ finish(UpgradeAttempt.create(name, oldVersion, newVersion), succeededAttempts);
+ }
+
+ @Override
+ public synchronized void onFailure(String name, int oldVersion, int newVersion) {
+ finish(UpgradeAttempt.create(name, oldVersion, newVersion), failedAttempts);
+ }
+
+ private synchronized void finish(UpgradeAttempt a, List<UpgradeAttempt> out) {
+ checkState(readyToStart.getCount() == 0, "shouldn't be finishing upgrade before starting");
+ checkState(
+ finished.getCount() > 0, "already finished %s upgrades, can't finish %s", numExpected, a);
+ out.add(a);
+ finished.countDown();
+ }
+
+ void runUpgrades() throws Exception {
+ readyToStart.countDown();
+ started.await();
+ finished.await();
+ }
+
+ synchronized ImmutableList<UpgradeAttempt> getStartedAttempts() {
+ return ImmutableList.copyOf(startedAttempts);
+ }
+
+ synchronized ImmutableList<UpgradeAttempt> getSucceededAttempts() {
+ return ImmutableList.copyOf(succeededAttempts);
+ }
+
+ synchronized ImmutableList<UpgradeAttempt> getFailedAttempts() {
+ return ImmutableList.copyOf(failedAttempts);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
index 2dcaf7d..94250e6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -14,38 +14,48 @@
package com.google.gerrit.acceptance.pgm;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
+import com.google.common.collect.ImmutableSet;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Provider;
import java.nio.file.Files;
+import java.util.Set;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
import org.junit.Test;
@NoHttpd
public class ReindexIT extends StandaloneSiteTest {
+ private static final String CHANGES = ChangeSchemaDefinitions.NAME;
+
+ private Project.NameKey project;
+ private String changeId;
+
@Test
public void reindexFromScratch() throws Exception {
- Project.NameKey project = new Project.NameKey("project");
- String changeId;
- try (ServerContext ctx = startServer()) {
- GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
- gApi.projects().create("project");
-
- ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
- in.newBranch = true;
- changeId = gApi.changes().create(in).info().changeId;
- }
+ setUpChange();
MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
Files.createDirectory(sitePaths.index_dir);
assertServerStartupFails();
runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+ assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
try (ServerContext ctx = startServer()) {
GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
@@ -53,4 +63,105 @@
.containsExactly(changeId);
}
}
+
+ @Test
+ public void onlineUpgradeChanges() throws Exception {
+ int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
+ int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
+
+ // Before storing any changes, switch back to the previous version.
+ GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+ status.setReady(CHANGES, currVersion, false);
+ status.setReady(CHANGES, prevVersion, true);
+ status.save();
+ assertReady(prevVersion);
+
+ setOnlineUpgradeConfig(false);
+ setUpChange();
+ setOnlineUpgradeConfig(true);
+
+ IndexUpgradeController u = new IndexUpgradeController(1);
+ try (ServerContext ctx = startServer(u.module())) {
+ assertSearchVersion(ctx, prevVersion);
+ assertWriteVersions(ctx, prevVersion, currVersion);
+
+ // Updating and searching old schema version works.
+ Provider<InternalChangeQuery> queryProvider =
+ ctx.getInjector().getProvider(InternalChangeQuery.class);
+ assertThat(queryProvider.get().byKey(new Change.Key(changeId))).hasSize(1);
+ assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
+
+ GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+ gApi.changes().id(changeId).topic("topic1");
+ assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
+
+ u.runUpgrades();
+ assertThat(u.getStartedAttempts())
+ .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
+ assertThat(u.getSucceededAttempts())
+ .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
+ assertThat(u.getFailedAttempts()).isEmpty();
+
+ assertReady(currVersion);
+ assertSearchVersion(ctx, currVersion);
+ assertWriteVersions(ctx, currVersion);
+
+ // Updating and searching new schema version works.
+ assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
+ assertThat(queryProvider.get().byTopicOpen("topic2")).isEmpty();
+ gApi.changes().id(changeId).topic("topic2");
+ assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
+ assertThat(queryProvider.get().byTopicOpen("topic2")).hasSize(1);
+ }
+ }
+
+ private void setUpChange() throws Exception {
+ project = new Project.NameKey("project");
+ try (ServerContext ctx = startServer()) {
+ GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+ gApi.projects().create(project.get());
+
+ ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
+ in.newBranch = true;
+ changeId = gApi.changes().create(in).info().changeId;
+ }
+ }
+
+ private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+ FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+ cfg.load();
+ cfg.setBoolean("index", null, "onlineUpgrade", enable);
+ cfg.save();
+ }
+
+ private void assertSearchVersion(ServerContext ctx, int expected) {
+ assertThat(
+ ctx.getInjector()
+ .getInstance(ChangeIndexCollection.class)
+ .getSearchIndex()
+ .getSchema()
+ .getVersion())
+ .named("search version")
+ .isEqualTo(expected);
+ }
+
+ private void assertWriteVersions(ServerContext ctx, Integer... expected) {
+ assertThat(
+ ctx.getInjector()
+ .getInstance(ChangeIndexCollection.class)
+ .getWriteIndexes()
+ .stream()
+ .map(i -> i.getSchema().getVersion()))
+ .named("write versions")
+ .containsExactlyElementsIn(ImmutableSet.copyOf(expected));
+ }
+
+ private void assertReady(int expectedReady) throws Exception {
+ Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
+ GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+ assertThat(
+ allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v))))
+ .named("ready state for index versions")
+ .isEqualTo(allVersions.stream().collect(toImmutableMap(v -> v, v -> v == expectedReady)));
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/OfflineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
similarity index 79%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/OfflineNoteDbMigrationIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
index 4b3e703..6ba5b07 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/OfflineNoteDbMigrationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
@@ -29,6 +29,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.notedb.ConfigNotesMigration;
import com.google.gerrit.server.notedb.NoteDbChangeState;
@@ -49,14 +50,15 @@
import org.junit.Test;
/**
- * Tests for offline {@code migrate-to-note-db} program.
+ * Tests for NoteDb migrations where the entry point is through a program, {@code
+ * migrate-to-note-db} or {@code daemon}.
*
* <p><strong>Note:</strong> These tests are very slow due to the repeated daemon startup. Prefer
* adding tests to {@link com.google.gerrit.acceptance.server.notedb.OnlineNoteDbMigrationIT} if
* possible.
*/
@NoHttpd
-public class OfflineNoteDbMigrationIT extends StandaloneSiteTest {
+public class StandaloneNoteDbMigrationIT extends StandaloneSiteTest {
private StoredConfig gerritConfig;
private Project.NameKey project;
@@ -145,6 +147,36 @@
assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
}
+ @Test
+ public void onlineMigrationViaDaemon() throws Exception {
+ assertNotesMigrationState(NotesMigrationState.REVIEW_DB);
+ int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
+ int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
+
+ // Before storing any changes, switch back to the previous version.
+ GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+ status.setReady(ChangeSchemaDefinitions.NAME, currVersion, false);
+ status.setReady(ChangeSchemaDefinitions.NAME, prevVersion, true);
+ status.save();
+
+ setOnlineUpgradeConfig(false);
+ setUpOneChange();
+ setOnlineUpgradeConfig(true);
+
+ IndexUpgradeController u = new IndexUpgradeController(1);
+ try (ServerContext ctx = startServer(u.module(), "--migrate-to-note-db", "true")) {
+ ChangeIndexCollection indexes = ctx.getInjector().getInstance(ChangeIndexCollection.class);
+ assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(prevVersion);
+
+ // Index schema upgrades happen after NoteDb migration, so waiting for those to complete
+ // should be sufficient.
+ u.runUpgrades();
+
+ assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(currVersion);
+ assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
+ }
+ }
+
private void setUpOneChange() throws Exception {
project = new Project.NameKey("project");
try (ServerContext ctx = startServer()) {
@@ -175,4 +207,10 @@
.getInstance(Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}, ReviewDbFactory.class))
.open();
}
+
+ private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+ gerritConfig.load();
+ gerritConfig.setBoolean("index", null, "onlineUpgrade", enable);
+ gerritConfig.save();
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 92cf30e..2c6b32f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,11 +14,13 @@
package com.google.gerrit.acceptance.rest.project;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -28,7 +30,6 @@
import org.junit.Before;
import org.junit.Test;
-@NoHttpd
public class DeleteBranchIT extends AbstractDaemonTest {
private Branch.NameKey branch;
@@ -86,6 +87,15 @@
assertDeleteSucceeds();
}
+ @Test
+ public void deleteBranchByRestWithoutRefsHeadsPrefix() throws Exception {
+ grantDelete();
+ String ref = branch.getShortName();
+ assertThat(ref).doesNotMatch(R_HEADS);
+ RestResponse r = userRestSession.delete("/projects/" + project.get() + "/branches/" + ref);
+ r.assertNoContent();
+ }
+
private void blockForcePush() throws Exception {
block("refs/heads/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index 1ca6c15..dc18a58 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -16,6 +16,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefNames;
+import static java.util.stream.Collectors.toList;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
@@ -37,7 +39,7 @@
@NoHttpd
public class DeleteBranchesIT extends AbstractDaemonTest {
private static final ImmutableList<String> BRANCHES =
- ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "refs/heads/test-3");
+ ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "test-3");
@Before
public void setUp() throws Exception {
@@ -138,7 +140,7 @@
for (String branch : branches) {
message
.append("Cannot delete ")
- .append(branch)
+ .append(prefixRef(branch))
.append(": it doesn't exist or you do not have permission ")
.append("to delete it\n");
}
@@ -156,17 +158,22 @@
private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions) throws Exception {
for (String branch : revisions.keySet()) {
RevCommit revision = revisions.get(branch);
- eventRecorder.assertRefUpdatedEvents(project.get(), branch, null, revision, revision, null);
+ eventRecorder.assertRefUpdatedEvents(
+ project.get(), prefixRef(branch), null, revision, revision, null);
}
}
+ private String prefixRef(String ref) {
+ return ref.startsWith(R_HEADS) ? ref : R_HEADS + ref;
+ }
+
private ProjectApi project() throws Exception {
return gApi.projects().name(project.get());
}
private void assertBranches(List<String> branches) throws Exception {
List<String> expected = Lists.newArrayList("HEAD", RefNames.REFS_CONFIG, "refs/heads/master");
- expected.addAll(branches);
+ expected.addAll(branches.stream().map(b -> prefixRef(b)).collect(toList()));
assertRefNames(expected, project().branches().get());
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 40fe4ae..e37071e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -16,9 +16,10 @@
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.projects.TagApi;
import com.google.gerrit.extensions.api.projects.TagInput;
@@ -27,7 +28,6 @@
import org.junit.Before;
import org.junit.Test;
-@NoHttpd
public class DeleteTagIT extends AbstractDaemonTest {
private final String TAG = "refs/tags/test";
@@ -82,6 +82,14 @@
assertDeleteSucceeds();
}
+ @Test
+ public void deleteTagByRestWithoutRefsTagsPrefix() throws Exception {
+ grantDelete();
+ String ref = TAG.substring(R_TAGS.length());
+ RestResponse r = userRestSession.delete("/projects/" + project.get() + "/tags/" + ref);
+ r.assertNoContent();
+ }
+
private void blockForcePush() throws Exception {
block("refs/tags/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index 69cd29a..8f24609 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -15,6 +15,8 @@
package com.google.gerrit.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
@@ -36,7 +38,7 @@
@NoHttpd
public class DeleteTagsIT extends AbstractDaemonTest {
private static final ImmutableList<String> TAGS =
- ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3");
+ ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3", "test-4");
@Before
public void setUp() throws Exception {
@@ -112,7 +114,7 @@
for (String tag : tags) {
message
.append("Cannot delete ")
- .append(tag)
+ .append(prefixRef(tag))
.append(": it doesn't exist or you do not have permission ")
.append("to delete it\n");
}
@@ -122,18 +124,24 @@
private HashMap<String, RevCommit> initialRevisions(List<String> tags) throws Exception {
HashMap<String, RevCommit> result = new HashMap<>();
for (String tag : tags) {
- result.put(tag, getRemoteHead(project, tag));
+ String ref = prefixRef(tag);
+ result.put(ref, getRemoteHead(project, ref));
}
return result;
}
private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions) throws Exception {
for (String tag : revisions.keySet()) {
- RevCommit revision = revisions.get(tag);
- eventRecorder.assertRefUpdatedEvents(project.get(), tag, null, revision, revision, null);
+ RevCommit revision = revisions.get(prefixRef(tag));
+ eventRecorder.assertRefUpdatedEvents(
+ project.get(), prefixRef(tag), null, revision, revision, null);
}
}
+ private String prefixRef(String ref) {
+ return ref.startsWith(R_TAGS) ? ref : R_TAGS + ref;
+ }
+
private ProjectApi project() throws Exception {
return gApi.projects().name(project.get());
}
@@ -141,7 +149,9 @@
private void assertTags(List<String> expected) throws Exception {
List<TagInfo> actualTags = project().tags().get();
Iterable<String> actualNames = Iterables.transform(actualTags, b -> b.ref);
- assertThat(actualNames).containsExactlyElementsIn(expected).inOrder();
+ assertThat(actualNames)
+ .containsExactlyElementsIn(expected.stream().map(t -> prefixRef(t)).collect(toList()))
+ .inOrder();
}
private void assertTagsDeleted() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index bb72a58..29b8ee7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -104,11 +104,15 @@
assertMigrationException(
"Cannot set both changes and projects", b -> b.setChanges(cs).setProjects(ps), m -> {});
assertMigrationException(
- "Cannot set changes or projects during auto-migration",
+ "Auto-migration cannot be used with trial mode",
+ b -> b.setAutoMigrate(true).setTrialMode(true),
+ m -> {});
+ assertMigrationException(
+ "Cannot set changes or projects during full migration",
b -> b.setChanges(cs),
NoteDbMigrator::migrate);
assertMigrationException(
- "Cannot set changes or projects during auto-migration",
+ "Cannot set changes or projects during full migration",
b -> b.setProjects(ps),
NoteDbMigrator::migrate);
@@ -294,49 +298,85 @@
}
@Test
- public void fullMigration() throws Exception {
- PushOneCommit.Result r = createChange();
- Change.Id id = r.getChange().getId();
+ public void fullMigrationSameThread() throws Exception {
+ testFullMigration(1);
+ }
- migrate(b -> b);
+ @Test
+ public void fullMigrationMultipleThreads() throws Exception {
+ testFullMigration(2);
+ }
+
+ private void testFullMigration(int threads) throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ Change.Id id1 = r1.getChange().getId();
+ Change.Id id2 = r2.getChange().getId();
+
+ migrate(b -> b.setThreads(threads));
assertNotesMigrationState(NOTE_DB_UNFUSED);
- assertThat(sequences.nextChangeId()).isEqualTo(502);
+ assertThat(sequences.nextChangeId()).isEqualTo(503);
- ObjectId oldMetaId;
- int rowVersion;
+ ObjectId oldMetaId = null;
+ int rowVersion = 0;
try (ReviewDb db = schemaFactory.open();
Repository repo = repoManager.openRepository(project)) {
- Ref ref = repo.exactRef(RefNames.changeMetaRef(id));
- assertThat(ref).isNotNull();
- oldMetaId = ref.getObjectId();
+ for (Change.Id id : ImmutableList.of(id1, id2)) {
+ String refName = RefNames.changeMetaRef(id);
+ Ref ref = repo.exactRef(refName);
+ assertThat(ref).named(refName).isNotNull();
- Change c = db.changes().get(id);
- assertThat(c.getTopic()).isNull();
- rowVersion = c.getRowVersion();
- NoteDbChangeState s = NoteDbChangeState.parse(c);
- assertThat(s.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB);
- assertThat(s.getRefState()).isEmpty();
+ Change c = db.changes().get(id);
+ assertThat(c.getTopic()).named("topic of change %s", id).isNull();
+ NoteDbChangeState s = NoteDbChangeState.parse(c);
+ assertThat(s.getPrimaryStorage())
+ .named("primary storage of change %s", id)
+ .isEqualTo(PrimaryStorage.NOTE_DB);
+ assertThat(s.getRefState()).named("ref state of change %s").isEmpty();
+
+ if (id.equals(id1)) {
+ oldMetaId = ref.getObjectId();
+ rowVersion = c.getRowVersion();
+ }
+ }
}
// Do not open a new context, to simulate races with other threads that opened a context earlier
// in the migration process; this needs to work.
- gApi.changes().id(id.get()).topic(name("a-topic"));
+ gApi.changes().id(id1.get()).topic(name("a-topic"));
// Of course, it should also work with a new context.
resetCurrentApiUser();
- gApi.changes().id(id.get()).topic(name("another-topic"));
+ gApi.changes().id(id1.get()).topic(name("another-topic"));
try (ReviewDb db = schemaFactory.open();
Repository repo = repoManager.openRepository(project)) {
- assertThat(repo.exactRef(RefNames.changeMetaRef(id)).getObjectId()).isNotEqualTo(oldMetaId);
+ assertThat(repo.exactRef(RefNames.changeMetaRef(id1)).getObjectId()).isNotEqualTo(oldMetaId);
- Change c = db.changes().get(id);
+ Change c = db.changes().get(id1);
assertThat(c.getTopic()).isNull();
assertThat(c.getRowVersion()).isEqualTo(rowVersion);
}
}
+ @Test
+ public void autoMigrationConfig() throws Exception {
+ createChange();
+
+ migrate(b -> b.setStopAtStateForTesting(WRITE));
+ assertNotesMigrationState(WRITE);
+ assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse();
+
+ migrate(b -> b.setAutoMigrate(true).setStopAtStateForTesting(READ_WRITE_NO_SEQUENCE));
+ assertNotesMigrationState(READ_WRITE_NO_SEQUENCE);
+ assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isTrue();
+
+ migrate(b -> b);
+ assertNotesMigrationState(NOTE_DB_UNFUSED);
+ assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse();
+ }
+
private void assertNotesMigrationState(NotesMigrationState expected) throws Exception {
assertThat(NotesMigrationState.forNotesMigration(notesMigration)).hasValue(expected);
gerritConfig.load();
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 2108815..a690136 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -14,36 +14,51 @@
package com.google.gerrit.elasticsearch;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.OnlineUpgrader;
import com.google.gerrit.server.index.SingleVersionModule;
+import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
-public class ElasticIndexModule extends LifecycleModule {
- private final int threads;
- private final Map<String, Integer> singleVersions;
-
+public class ElasticIndexModule extends AbstractModule {
public static ElasticIndexModule singleVersionWithExplicitVersions(
Map<String, Integer> versions, int threads) {
- return new ElasticIndexModule(versions, threads);
+ return new ElasticIndexModule(versions, threads, false);
}
public static ElasticIndexModule latestVersionWithOnlineUpgrade() {
- return new ElasticIndexModule(null, 0);
+ return new ElasticIndexModule(null, 0, true);
}
- private ElasticIndexModule(Map<String, Integer> singleVersions, int threads) {
+ public static ElasticIndexModule latestVersionWithoutOnlineUpgrade() {
+ return new ElasticIndexModule(null, 0, false);
+ }
+
+ private final Map<String, Integer> singleVersions;
+ private final int threads;
+ private final boolean onlineUpgrade;
+
+ private ElasticIndexModule(
+ Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade) {
+ if (singleVersions != null) {
+ checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
+ }
this.singleVersions = singleVersions;
this.threads = threads;
+ this.onlineUpgrade = onlineUpgrade;
}
@Override
@@ -63,7 +78,7 @@
install(new IndexModule(threads));
if (singleVersions == null) {
- listener().to(ElasticVersionManager.class);
+ install(new MultiVersionModule());
} else {
install(new SingleVersionModule(singleVersions));
}
@@ -74,4 +89,15 @@
IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
}
+
+ private class MultiVersionModule extends LifecycleModule {
+ @Override
+ public void configure() {
+ bind(VersionManager.class).to(ElasticVersionManager.class);
+ listener().to(ElasticVersionManager.class);
+ if (onlineUpgrade) {
+ listener().to(OnlineUpgrader.class);
+ }
+ }
+ }
}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
index 74a6b69..609c4d9 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
@@ -16,14 +16,15 @@
import com.google.common.base.MoreObjects;
import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.AbstractVersionManager;
import com.google.gerrit.server.index.GerritIndexStatus;
import com.google.gerrit.server.index.Index;
import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.VersionManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -34,7 +35,7 @@
import org.slf4j.LoggerFactory;
@Singleton
-public class ElasticVersionManager extends AbstractVersionManager implements LifecycleListener {
+public class ElasticVersionManager extends VersionManager {
private static final Logger log = LoggerFactory.getLogger(ElasticVersionManager.class);
private final String prefix;
@@ -44,9 +45,10 @@
ElasticVersionManager(
@GerritServerConfig Config cfg,
SitePaths sitePaths,
+ DynamicSet<OnlineUpgradeListener> listeners,
Collection<IndexDefinition<?, ?, ?>> defs,
ElasticIndexVersionDiscovery versionDiscovery) {
- super(cfg, sitePaths, defs);
+ super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
this.versionDiscovery = versionDiscovery;
prefix = MoreObjects.firstNonNull(cfg.getString("index", null, "prefix"), "gerrit");
}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 524af50..669b610 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -37,7 +37,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -71,8 +70,6 @@
/** Unit tests for {@link GerritPublicKeyChecker}. */
public class GerritPublicKeyCheckerTest {
- @Inject private Accounts accounts;
-
@Inject private AccountsUpdate.Server accountsUpdate;
@Inject private AccountManager accountManager;
@@ -117,10 +114,8 @@
db = schemaFactory.open();
schemaCreator.create(db);
userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
- Account userAccount = accounts.get(db, userId);
// Note: does not match any key in TestKeys.
- userAccount.setPreferredEmail("user@example.com");
- accountsUpdate.create().update(db, userAccount);
+ accountsUpdate.create().update(db, userId, a -> a.setPreferredEmail("user@example.com"));
user = reloadUser();
requestContext.setContext(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 7a1a450..753d421 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -53,7 +53,6 @@
if ("self".startsWith(request.getQuery())) {
final ArrayList<SuggestOracle.Suggestion> r =
new ArrayList<>(response.getSuggestions().size() + 1);
- r.addAll(response.getSuggestions());
r.add(
new SuggestOracle.Suggestion() {
@Override
@@ -66,6 +65,7 @@
return "self";
}
});
+ r.addAll(response.getSuggestions());
response.setSuggestions(r);
}
done.onSuggestionsReady(request, response);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 15169ac..8bbc988 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -542,16 +542,24 @@
}
}
- Configuration cfg = Configuration.create().set("autoCloseBrackets", prefs.autoCloseBrackets())
- .set("cursorBlinkRate", prefs.cursorBlinkRate()).set("cursorHeight", 0.85)
- .set("indentUnit", prefs.indentUnit())
- .set("keyMap", prefs.keyMapType().name().toLowerCase())
- .set("lineNumbers", prefs.hideLineNumbers()).set("lineWrapping", false)
- .set("matchBrackets", prefs.matchBrackets()).set("mode", mode != null ? mode.mime() : null)
- .set("origLeft", editContent).set("scrollbarStyle", "overlay")
- .set("showTrailingSpace", prefs.showWhitespaceErrors()).set("styleSelectedText", true)
- .set("tabSize", prefs.tabSize()).set("theme", prefs.theme().name().toLowerCase())
- .set("value", "");
+ Configuration cfg =
+ Configuration.create()
+ .set("autoCloseBrackets", prefs.autoCloseBrackets())
+ .set("cursorBlinkRate", prefs.cursorBlinkRate())
+ .set("cursorHeight", 0.85)
+ .set("indentUnit", prefs.indentUnit())
+ .set("keyMap", prefs.keyMapType().name().toLowerCase())
+ .set("lineNumbers", prefs.hideLineNumbers())
+ .set("lineWrapping", false)
+ .set("matchBrackets", prefs.matchBrackets())
+ .set("mode", mode != null ? mode.mime() : null)
+ .set("origLeft", editContent)
+ .set("scrollbarStyle", "overlay")
+ .set("showTrailingSpace", prefs.showWhitespaceErrors())
+ .set("styleSelectedText", true)
+ .set("tabSize", prefs.tabSize())
+ .set("theme", prefs.theme().name().toLowerCase())
+ .set("value", "");
if (editContent.contains("\r\n")) {
cfg.set("lineSeparator", "\r\n");
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 47850c4..0ee720a 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
@@ -42,6 +42,7 @@
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -111,7 +112,7 @@
Account target;
try {
target = accountResolver.find(db.get(), runas);
- } catch (OrmException e) {
+ } 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);
return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index ec7fb68..e721b7a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -49,6 +49,7 @@
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -231,7 +232,7 @@
}
try (ReviewDb db = schema.open()) {
return auth(accounts.get(db, id));
- } catch (OrmException e) {
+ } catch (OrmException | IOException | ConfigInvalidException e) {
getServletContext().log("cannot query database", e);
return null;
}
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 6dbebf1..558cc78 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
@@ -44,6 +44,12 @@
<link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n}
<script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
+ // Content between webcomponents-lite and the load of the main app element
+ // run before polymer-resin is installed so may have security consequences.
+ // Contact your local security engineer if you have any questions, and
+ // CC them on any changes that load content before gr-app.html.
+ //
+ // github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
<link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n}
<link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 89fd819..b5531d5 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -14,15 +14,20 @@
package com.google.gerrit.lucene;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.OnlineUpgrader;
import com.google.gerrit.server.index.SingleVersionModule;
+import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.FactoryModuleBuilder;
@@ -30,30 +35,40 @@
import org.apache.lucene.search.BooleanQuery;
import org.eclipse.jgit.lib.Config;
-public class LuceneIndexModule extends LifecycleModule {
+public class LuceneIndexModule extends AbstractModule {
public static LuceneIndexModule singleVersionAllLatest(int threads) {
- return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads);
+ return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads, false);
}
public static LuceneIndexModule singleVersionWithExplicitVersions(
Map<String, Integer> versions, int threads) {
- return new LuceneIndexModule(versions, threads);
+ return new LuceneIndexModule(versions, threads, false);
}
public static LuceneIndexModule latestVersionWithOnlineUpgrade() {
- return new LuceneIndexModule(null, 0);
+ return new LuceneIndexModule(null, 0, true);
+ }
+
+ public static LuceneIndexModule latestVersionWithoutOnlineUpgrade() {
+ return new LuceneIndexModule(null, 0, false);
}
static boolean isInMemoryTest(Config cfg) {
return cfg.getBoolean("index", "lucene", "testInmemory", false);
}
- private final int threads;
private final Map<String, Integer> singleVersions;
+ private final int threads;
+ private final boolean onlineUpgrade;
- private LuceneIndexModule(Map<String, Integer> singleVersions, int threads) {
+ private LuceneIndexModule(
+ Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade) {
+ if (singleVersions != null) {
+ checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
+ }
this.singleVersions = singleVersions;
this.threads = threads;
+ this.onlineUpgrade = onlineUpgrade;
}
@Override
@@ -87,10 +102,14 @@
return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
}
- private static class MultiVersionModule extends LifecycleModule {
+ private class MultiVersionModule extends LifecycleModule {
@Override
public void configure() {
+ bind(VersionManager.class).to(LuceneVersionManager.class);
listener().to(LuceneVersionManager.class);
+ if (onlineUpgrade) {
+ listener().to(OnlineUpgrader.class);
+ }
}
}
}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index ad13066..9e8007c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -15,14 +15,15 @@
package com.google.gerrit.lucene;
import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.AbstractVersionManager;
import com.google.gerrit.server.index.GerritIndexStatus;
import com.google.gerrit.server.index.Index;
import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.VersionManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -36,10 +37,10 @@
import org.slf4j.LoggerFactory;
@Singleton
-public class LuceneVersionManager extends AbstractVersionManager implements LifecycleListener {
+public class LuceneVersionManager extends VersionManager {
private static final Logger log = LoggerFactory.getLogger(LuceneVersionManager.class);
- private static class Version<V> extends AbstractVersionManager.Version<V> {
+ private static class Version<V> extends VersionManager.Version<V> {
private final boolean exists;
private Version(Schema<V> schema, int version, boolean exists, boolean ready) {
@@ -56,22 +57,22 @@
LuceneVersionManager(
@GerritServerConfig Config cfg,
SitePaths sitePaths,
+ DynamicSet<OnlineUpgradeListener> listeners,
Collection<IndexDefinition<?, ?, ?>> defs) {
- super(cfg, sitePaths, defs);
+ super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
}
@Override
protected <V> boolean isDirty(
- Collection<com.google.gerrit.server.index.AbstractVersionManager.Version<V>> inUse,
- com.google.gerrit.server.index.AbstractVersionManager.Version<V> v) {
+ Collection<com.google.gerrit.server.index.VersionManager.Version<V>> inUse,
+ com.google.gerrit.server.index.VersionManager.Version<V> v) {
return !inUse.contains(v) && ((Version<V>) v).exists;
}
@Override
- protected <K, V, I extends Index<K, V>>
- TreeMap<Integer, AbstractVersionManager.Version<V>> scanVersions(
- IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
- TreeMap<Integer, AbstractVersionManager.Version<V>> versions = new TreeMap<>();
+ protected <K, V, I extends Index<K, V>> TreeMap<Integer, VersionManager.Version<V>> scanVersions(
+ IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
+ TreeMap<Integer, VersionManager.Version<V>> versions = new TreeMap<>();
for (Schema<V> schema : def.getSchemas().values()) {
// This part is Lucene-specific.
Path p = getDir(sitePaths, def.getName(), schema);
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index ea90c3e..b58891b 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -9,6 +9,7 @@
INIT_API_SRCS = glob([SRCS + "init/api/*.java"])
BASE_JETTY_DEPS = [
+ "//gerrit-common:annotations",
"//gerrit-common:server",
"//gerrit-extension-api:api",
"//gerrit-gwtexpui:linker_server",
@@ -35,7 +36,7 @@
name = "init-api",
srcs = INIT_API_SRCS,
visibility = ["//visibility:public"],
- deps = DEPS + ["//gerrit-common:annotations"],
+ deps = DEPS,
)
java_library(
@@ -46,7 +47,6 @@
deps = DEPS + [
":init-api",
":util",
- "//gerrit-common:annotations",
"//gerrit-elasticsearch:elasticsearch",
"//gerrit-launcher:launcher", # We want this dep to be provided_deps
"//gerrit-lucene:lucene",
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 fc2fdef..757b130 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
@@ -14,12 +14,14 @@
package com.google.gerrit.pgm;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.elasticsearch.ElasticIndexModule;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.gpg.GpgModule;
@@ -69,10 +71,13 @@
import com.google.gerrit.server.index.DummyIndexModule;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.receive.MailReceiver;
import com.google.gerrit.server.mail.send.SmtpEmailSender;
import com.google.gerrit.server.mime.MimeUtil2Module;
+import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
+import com.google.gerrit.server.notedb.rebuild.OnlineNoteDbMigrator;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
import com.google.gerrit.server.plugins.PluginModule;
@@ -111,6 +116,7 @@
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -162,6 +168,13 @@
@Option(name = "--stop-only", usage = "Stop the daemon", hidden = true)
private boolean stopOnly;
+ @Option(
+ name = "--migrate-to-note-db",
+ usage = "(EXPERIMENTAL) Automatically migrate changes to NoteDb",
+ handler = ExplicitBooleanOptionHandler.class
+ )
+ private boolean migrateToNoteDb;
+
private final LifecycleManager manager = new LifecycleManager();
private Injector dbInjector;
private Injector cfgInjector;
@@ -174,6 +187,7 @@
private boolean test;
private AbstractModule luceneModule;
private Module emailModule;
+ private Module testSysModule;
private Runnable serverStarted;
private IndexType indexType;
@@ -297,6 +311,11 @@
}
@VisibleForTesting
+ public void setAdditionalSysModuleForTesting(@Nullable Module m) {
+ testSysModule = m;
+ }
+
+ @VisibleForTesting
public void start() throws IOException {
if (dbInjector == null) {
dbInjector = createDbInjector(true /* enableMetrics */, MULTI_USER);
@@ -442,9 +461,19 @@
modules.add(new ChangeCleanupRunner.Module());
}
modules.addAll(LibModuleLoader.loadModules(cfgInjector));
+ if (migrateToNoteDb()) {
+ modules.add(new OnlineNoteDbMigrator.Module());
+ }
+ if (testSysModule != null) {
+ modules.add(testSysModule);
+ }
return cfgInjector.createChildInjector(modules);
}
+ private boolean migrateToNoteDb() {
+ return migrateToNoteDb || NoteDbMigrator.getAutoMigrate(checkNotNull(config));
+ }
+
private Module createIndexModule() {
if (slave) {
return new DummyIndexModule();
@@ -452,11 +481,19 @@
if (luceneModule != null) {
return luceneModule;
}
+ boolean onlineUpgrade =
+ VersionManager.getOnlineUpgrade(config)
+ // Schema upgrade is handled by OnlineNoteDbMigrator in this case.
+ && !migrateToNoteDb();
switch (indexType) {
case LUCENE:
- return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+ return onlineUpgrade
+ ? LuceneIndexModule.latestVersionWithOnlineUpgrade()
+ : LuceneIndexModule.latestVersionWithoutOnlineUpgrade();
case ELASTICSEARCH:
- return ElasticIndexModule.latestVersionWithOnlineUpgrade();
+ return onlineUpgrade
+ ? ElasticIndexModule.latestVersionWithOnlineUpgrade()
+ : ElasticIndexModule.latestVersionWithoutOnlineUpgrade();
default:
throw new IllegalStateException("unsupported index.type = " + indexType);
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index 9465a54..81435e0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -19,11 +19,13 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.File;
@@ -57,7 +59,15 @@
ObjectInserter oi = repo.newObjectInserter()) {
PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get();
AccountsUpdate.createUserBranch(
- repo, oi, serverIdent, serverIdent, account.getId(), account.getRegisteredOn());
+ repo,
+ new Project.NameKey(allUsers),
+ GitReferenceUpdated.DISABLED,
+ null,
+ oi,
+ serverIdent,
+ serverIdent,
+ account.getId(),
+ account.getRegisteredOn());
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index b79f496..2fad708 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -44,6 +44,7 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@@ -60,6 +61,7 @@
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.commons.lang.mutable.MutableDouble;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,7 +75,7 @@
new double[] {
BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,
};
- private static final long PLUGIN_QUERY_TIMEOUT = 500; //ms
+ private static final long PLUGIN_QUERY_TIMEOUT = 500; // ms
private final ChangeQueryBuilder changeQueryBuilder;
private final Config config;
@@ -108,7 +110,7 @@
SuggestReviewers suggestReviewers,
ProjectControl projectControl,
List<Account.Id> candidateList)
- throws OrmException {
+ throws OrmException, IOException, ConfigInvalidException {
String query = suggestReviewers.getQuery();
double baseWeight = config.getInt("addReviewer", "baseWeight", 1);
@@ -196,7 +198,7 @@
}
private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(double baseWeight)
- throws OrmException {
+ throws OrmException, IOException, ConfigInvalidException {
// Get the user's last 25 changes, check approvals
try {
List<ChangeData> result =
@@ -225,7 +227,7 @@
private Map<Account.Id, MutableDouble> baseRankingForCandidateList(
List<Account.Id> candidates, ProjectControl projectControl, double baseWeight)
- throws OrmException {
+ throws OrmException, IOException, ConfigInvalidException {
// Get each reviewer's activity based on number of applied labels
// (weighted 10d), number of comments (weighted 0.5d) and number of owned
// changes (weighted 1d).
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 410dc5c..d3083e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -56,6 +56,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
public class ReviewersUtil {
@Singleton
@@ -146,7 +147,7 @@
ProjectControl projectControl,
VisibilityControl visibilityControl,
boolean excludeGroups)
- throws IOException, OrmException {
+ throws IOException, OrmException, ConfigInvalidException {
String query = suggestReviewers.getQuery();
int limit = suggestReviewers.getLimit();
@@ -212,7 +213,7 @@
SuggestReviewers suggestReviewers,
ProjectControl projectControl,
List<Account.Id> candidateList)
- throws OrmException {
+ throws OrmException, IOException, ConfigInvalidException {
try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
return reviewerRecommender.suggestReviewers(
changeNotes, suggestReviewers, projectControl, candidateList);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
index f1f0e6f..010ed32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
@@ -18,6 +18,11 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer2;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -32,18 +37,22 @@
import java.util.List;
import org.eclipse.jgit.lib.Config;
-@SuppressWarnings("deprecation")
@Singleton
public class Sequences {
- public static final String CHANGES = "changes";
+ public static final String NAME_CHANGES = "changes";
public static int getChangeSequenceGap(Config cfg) {
return cfg.getInt("noteDb", "changes", "initialSequenceGap", 1000);
}
+ private enum SequenceType {
+ CHANGES;
+ }
+
private final Provider<ReviewDb> db;
private final NotesMigration migration;
private final RepoSequence changeSeq;
+ private final Timer2<SequenceType, Boolean> nextIdLatency;
@Inject
Sequences(
@@ -51,30 +60,41 @@
Provider<ReviewDb> db,
NotesMigration migration,
GitRepositoryManager repoManager,
- AllProjectsName allProjects) {
+ AllProjectsName allProjects,
+ MetricMaker metrics) {
this.db = db;
this.migration = migration;
int gap = getChangeSequenceGap(cfg);
- changeSeq =
- new RepoSequence(
- repoManager,
- allProjects,
- CHANGES,
- () -> db.get().nextChangeId() + gap,
- cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20));
+ @SuppressWarnings("deprecation")
+ RepoSequence.Seed seed = () -> db.get().nextChangeId() + gap;
+ int batchSize = cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20);
+ changeSeq = new RepoSequence(repoManager, allProjects, NAME_CHANGES, seed, batchSize);
+
+ nextIdLatency =
+ metrics.newTimer(
+ "sequence/next_id_latency",
+ new Description("Latency of requesting IDs from repo sequences")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ Field.ofEnum(SequenceType.class, "sequence"),
+ Field.ofBoolean("multiple"));
}
public int nextChangeId() throws OrmException {
if (!migration.readChangeSequence()) {
- return db.get().nextChangeId();
+ return nextChangeId(db.get());
}
- return changeSeq.next();
+ try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) {
+ return changeSeq.next();
+ }
}
public ImmutableList<Integer> nextChangeIds(int count) throws OrmException {
if (migration.readChangeSequence()) {
- return changeSeq.next(count);
+ try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) {
+ return changeSeq.next(count);
+ }
}
if (count == 0) {
@@ -84,7 +104,7 @@
List<Integer> ids = new ArrayList<>(count);
ReviewDb db = this.db.get();
for (int i = 0; i < count; i++) {
- ids.add(db.nextChangeId());
+ ids.add(nextChangeId(db));
}
return ImmutableList.copyOf(ids);
}
@@ -93,4 +113,9 @@
public RepoSequence getChangeIdRepoSequence() {
return changeSeq;
}
+
+ @SuppressWarnings("deprecation")
+ private static int nextChangeId(ReviewDb db) throws OrmException {
+ return db.nextChangeId();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
new file mode 100644
index 0000000..70ed29e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
@@ -0,0 +1,260 @@
+// 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.common.base.Preconditions.checkState;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevSort;
+
+/**
+ * ‘account.config’ file in the user branch in the All-Users repository that contains the properties
+ * of the account.
+ *
+ * <p>The 'account.config' file is a git config file that has one 'account' section with the
+ * properties of the account:
+ *
+ * <pre>
+ * [account]
+ * active = false
+ * fullName = John Doe
+ * preferredEmail = john.doe@foo.com
+ * status = Overloaded with reviews
+ * </pre>
+ *
+ * <p>All keys are optional. This means 'account.config' may not exist on the user branch if no
+ * properties are set.
+ *
+ * <p>Not setting a key and setting a key to an empty string are treated the same way and result in
+ * a {@code null} value.
+ *
+ * <p>If no value for 'active' is specified, by default the account is considered as active.
+ *
+ * <p>The commit date of the first commit on the user branch is used as registration date of the
+ * account. The first commit may be an empty commit (if no properties were set and 'account.config'
+ * doesn't exist).
+ */
+public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
+ public static final String ACCOUNT_CONFIG = "account.config";
+ public static final String ACCOUNT = "account";
+ public static final String KEY_ACTIVE = "active";
+ public static final String KEY_FULL_NAME = "fullName";
+ public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
+ public static final String KEY_STATUS = "status";
+
+ @Nullable private final OutgoingEmailValidator emailValidator;
+ private final Account.Id accountId;
+ private final String ref;
+
+ private boolean isLoaded;
+ private Account account;
+ private Timestamp registeredOn;
+ private List<ValidationError> validationErrors;
+
+ public AccountConfig(@Nullable OutgoingEmailValidator emailValidator, Account.Id accountId) {
+ this.emailValidator = emailValidator;
+ this.accountId = accountId;
+ this.ref = RefNames.refsUsers(accountId);
+ }
+
+ @Override
+ protected String getRefName() {
+ return ref;
+ }
+
+ /**
+ * Get the loaded account.
+ *
+ * @return loaded account.
+ * @throws IllegalStateException if the account was not loaded yet
+ */
+ public Account getAccount() {
+ checkLoaded();
+ return account;
+ }
+
+ /**
+ * Sets the account. This means the loaded account will be overwritten with the given account.
+ *
+ * <p>Changing the registration date of an account is not supported.
+ *
+ * @param account account that should be set
+ * @throws IllegalStateException if the account was not loaded yet
+ */
+ public void setAccount(Account account) {
+ checkLoaded();
+ this.account = account;
+ this.registeredOn = account.getRegisteredOn();
+ }
+
+ /**
+ * Creates a new account.
+ *
+ * @return the new account
+ * @throws OrmDuplicateKeyException if the user branch already exists
+ */
+ public Account getNewAccount() throws OrmDuplicateKeyException {
+ checkLoaded();
+ if (revision != null) {
+ throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
+ }
+ this.registeredOn = TimeUtil.nowTs();
+ this.account = new Account(accountId, registeredOn);
+ return account;
+ }
+
+ @Override
+ protected void onLoad() throws IOException, ConfigInvalidException {
+ if (revision != null) {
+ rw.markStart(revision);
+ rw.sort(RevSort.REVERSE);
+ registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);
+
+ Config cfg = readConfig(ACCOUNT_CONFIG);
+
+ account = parse(cfg);
+ }
+
+ isLoaded = true;
+ }
+
+ private Account parse(Config cfg) {
+ Account account = new Account(accountId, registeredOn);
+ account.setActive(cfg.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
+ account.setFullName(get(cfg, KEY_FULL_NAME));
+
+ String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL);
+ account.setPreferredEmail(preferredEmail);
+ if (emailValidator != null && !emailValidator.isValid(preferredEmail)) {
+ error(
+ new ValidationError(
+ ACCOUNT_CONFIG, String.format("Invalid preferred email: %s", preferredEmail)));
+ }
+
+ account.setStatus(get(cfg, KEY_STATUS));
+ return account;
+ }
+
+ @Override
+ protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+ checkLoaded();
+
+ if (revision != null) {
+ commit.setMessage("Update account\n");
+ } else if (account != null) {
+ commit.setMessage("Create account\n");
+ commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
+ commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
+ }
+
+ Config cfg = readConfig(ACCOUNT_CONFIG);
+ setActive(cfg, account.isActive());
+ set(cfg, KEY_FULL_NAME, account.getFullName());
+ set(cfg, KEY_PREFERRED_EMAIL, account.getPreferredEmail());
+ set(cfg, KEY_STATUS, account.getStatus());
+ saveConfig(ACCOUNT_CONFIG, cfg);
+ return true;
+ }
+
+ /**
+ * Sets/Unsets {@code account.active} in the given config.
+ *
+ * <p>{@code account.active} is set to {@code false} if the account is inactive.
+ *
+ * <p>If the account is active {@code account.active} is unset since {@code true} is the default
+ * if this field is missing.
+ *
+ * @param cfg the config
+ * @param value whether the account is active
+ */
+ private static void setActive(Config cfg, boolean value) {
+ if (!value) {
+ cfg.setBoolean(ACCOUNT, null, KEY_ACTIVE, false);
+ } else {
+ cfg.unset(ACCOUNT, null, KEY_ACTIVE);
+ }
+ }
+
+ /**
+ * Sets/Unsets the given key in the given config.
+ *
+ * <p>The key unset if the value is {@code null}.
+ *
+ * @param cfg the config
+ * @param key the key
+ * @param value the value
+ */
+ private static void set(Config cfg, String key, String value) {
+ if (!Strings.isNullOrEmpty(value)) {
+ cfg.setString(ACCOUNT, null, key, value);
+ } else {
+ cfg.unset(ACCOUNT, null, key);
+ }
+ }
+
+ /**
+ * Gets the given key from the given config.
+ *
+ * <p>Empty values are returned as {@code null}
+ *
+ * @param cfg the config
+ * @param key the key
+ * @return the value, {@code null} if key was not set or key was set to empty string
+ */
+ private static String get(Config cfg, String key) {
+ return Strings.emptyToNull(cfg.getString(ACCOUNT, null, key));
+ }
+
+ private void checkLoaded() {
+ checkState(isLoaded, "account not loaded yet");
+ }
+
+ /**
+ * Get the validation errors, if any were discovered during load.
+ *
+ * @return list of errors; empty list if there are no errors.
+ */
+ public List<ValidationError> getValidationErrors() {
+ if (validationErrors != null) {
+ return ImmutableList.copyOf(validationErrors);
+ }
+ return ImmutableList.of();
+ }
+
+ @Override
+ public void error(ValidationError error) {
+ if (validationErrors == null) {
+ validationErrors = new ArrayList<>(4);
+ }
+ validationErrors.add(error);
+ }
+}
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 861667b..a6f1e2b 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
@@ -16,7 +16,6 @@
import com.google.common.base.Strings;
import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.Permission;
@@ -39,10 +38,13 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
@@ -150,7 +152,7 @@
private void update(ReviewDb db, AuthRequest who, ExternalId extId)
throws OrmException, IOException, ConfigInvalidException {
IdentifiedUser user = userFactory.create(extId.accountId());
- Account toUpdate = null;
+ List<Consumer<Account>> accountUpdates = new ArrayList<>();
// If the email address was modified by the authentication provider,
// update our records to match the changed email.
@@ -159,8 +161,7 @@
String oldEmail = extId.email();
if (newEmail != null && !newEmail.equals(oldEmail)) {
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
- toUpdate = load(toUpdate, user.getAccountId(), db);
- toUpdate.setPreferredEmail(newEmail);
+ accountUpdates.add(a -> a.setPreferredEmail(newEmail));
}
externalIdsUpdateFactory
@@ -172,8 +173,7 @@
if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
&& !Strings.isNullOrEmpty(who.getDisplayName())
&& !eq(user.getAccount().getFullName(), who.getDisplayName())) {
- toUpdate = load(toUpdate, user.getAccountId(), db);
- toUpdate.setFullName(who.getDisplayName());
+ accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
}
if (!realm.allowsEdit(AccountFieldName.USER_NAME)
@@ -184,8 +184,12 @@
"Not changing already set username %s to %s", user.getUserName(), who.getUserName()));
}
- if (toUpdate != null) {
- accountsUpdateFactory.create().update(db, toUpdate);
+ if (!accountUpdates.isEmpty()) {
+ Account account =
+ accountsUpdateFactory.create().update(db, user.getAccountId(), accountUpdates);
+ if (account == null) {
+ throw new OrmException("Account " + user.getAccountId() + " has been deleted");
+ }
}
if (newEmail != null && !newEmail.equals(oldEmail)) {
@@ -194,16 +198,6 @@
}
}
- private Account load(Account toUpdate, Account.Id accountId, ReviewDb db) throws OrmException {
- if (toUpdate == null) {
- toUpdate = accounts.get(db, accountId);
- if (toUpdate == null) {
- throw new OrmException("Account " + accountId + " has been deleted");
- }
- }
- return toUpdate;
- }
-
private static boolean eq(String a, String b) {
return (a == null && b == null) || (a != null && a.equals(b));
}
@@ -211,18 +205,23 @@
private AuthResult create(ReviewDb db, AuthRequest who)
throws OrmException, AccountException, IOException, ConfigInvalidException {
Account.Id newId = new Account.Id(db.nextAccountId());
- Account account = new Account(newId, TimeUtil.nowTs());
ExternalId extId =
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
- account.setFullName(who.getDisplayName());
- account.setPreferredEmail(extId.email());
boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false) && !accounts.hasAnyAccount();
+ Account account;
try {
AccountsUpdate accountsUpdate = accountsUpdateFactory.create();
- accountsUpdate.upsert(db, account);
+ account =
+ accountsUpdate.insert(
+ db,
+ newId,
+ a -> {
+ a.setFullName(who.getDisplayName());
+ a.setPreferredEmail(extId.email());
+ });
ExternalId existingExtId = externalIds.get(extId.key());
if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
@@ -364,11 +363,16 @@
.insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
if (who.getEmailAddress() != null) {
- Account a = accounts.get(db, to);
- if (a.getPreferredEmail() == null) {
- a.setPreferredEmail(who.getEmailAddress());
- accountsUpdateFactory.create().update(db, a);
- }
+ accountsUpdateFactory
+ .create()
+ .update(
+ db,
+ to,
+ a -> {
+ if (a.getPreferredEmail() == null) {
+ a.setPreferredEmail(who.getEmailAddress());
+ }
+ });
byEmailCache.evict(who.getEmailAddress());
}
}
@@ -428,12 +432,17 @@
externalIdsUpdateFactory.create().delete(extId);
if (who.getEmailAddress() != null) {
- Account a = accounts.get(db, from);
- if (a.getPreferredEmail() != null
- && a.getPreferredEmail().equals(who.getEmailAddress())) {
- a.setPreferredEmail(null);
- accountsUpdateFactory.create().update(db, a);
- }
+ accountsUpdateFactory
+ .create()
+ .update(
+ db,
+ from,
+ a -> {
+ if (a.getPreferredEmail() != null
+ && a.getPreferredEmail().equals(who.getEmailAddress())) {
+ a.setPreferredEmail(null);
+ }
+ });
byEmailCache.evict(who.getEmailAddress());
}
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 8ce5f4c..7f66b9c 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
@@ -23,12 +23,14 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class AccountResolver {
@@ -61,7 +63,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 {
+ public Account find(ReviewDb db, String nameOrEmail)
+ throws OrmException, IOException, ConfigInvalidException {
Set<Account.Id> r = findAll(db, nameOrEmail);
if (r.size() == 1) {
return byId.get(r.iterator().next()).getAccount();
@@ -90,7 +93,8 @@
* name ("username").
* @return the accounts that match, empty collection if none. Never null.
*/
- public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail) throws OrmException {
+ public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail)
+ throws OrmException, IOException, ConfigInvalidException {
Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
if (m.matches()) {
Account.Id id = Account.Id.parse(m.group(1));
@@ -118,7 +122,8 @@
return findAllByNameOrEmail(db, nameOrEmail);
}
- private boolean exists(ReviewDb db, Account.Id id) throws OrmException {
+ private boolean exists(ReviewDb db, Account.Id id)
+ throws OrmException, IOException, ConfigInvalidException {
return accounts.get(db, id) != null;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
index 1a82f7b..28ed422 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
@@ -22,35 +22,70 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
import com.google.gwtorm.server.OrmException;
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;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Class to access accounts. */
@Singleton
public class Accounts {
+ private static final Logger log = LoggerFactory.getLogger(Accounts.class);
+
+ private final boolean readFromGit;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
+ private final OutgoingEmailValidator emailValidator;
@Inject
- Accounts(GitRepositoryManager repoManager, AllUsersName allUsersName) {
+ Accounts(
+ @GerritServerConfig Config cfg,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ OutgoingEmailValidator emailValidator) {
+ this.readFromGit = cfg.getBoolean("user", null, "readAccountsFromGit", false);
this.repoManager = repoManager;
this.allUsersName = allUsersName;
+ this.emailValidator = emailValidator;
}
- public Account get(ReviewDb db, Account.Id accountId) throws OrmException {
+ public Account get(ReviewDb db, Account.Id accountId)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (readFromGit) {
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ return read(repo, accountId);
+ }
+ }
+
return db.accounts().get(accountId);
}
- public List<Account> get(ReviewDb db, Collection<Account.Id> accountIds) throws OrmException {
+ public List<Account> get(ReviewDb db, Collection<Account.Id> accountIds)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (readFromGit) {
+ List<Account> accounts = new ArrayList<>(accountIds.size());
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ for (Account.Id accountId : accountIds) {
+ accounts.add(read(repo, accountId));
+ }
+ }
+ return accounts;
+ }
+
return db.accounts().get(accountIds).toList();
}
@@ -59,7 +94,22 @@
*
* @return all accounts
*/
- public List<Account> all(ReviewDb db) throws OrmException {
+ public List<Account> all(ReviewDb db) throws OrmException, IOException {
+ if (readFromGit) {
+ Set<Account.Id> accountIds = allIds();
+ List<Account> accounts = new ArrayList<>(accountIds.size());
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ for (Account.Id accountId : accountIds) {
+ try {
+ accounts.add(read(repo, accountId));
+ } catch (Exception e) {
+ log.error(String.format("Ignoring invalid account %s", accountId.get()), e);
+ }
+ }
+ }
+ return accounts;
+ }
+
return db.accounts().all().toList();
}
@@ -103,6 +153,13 @@
}
}
+ private Account read(Repository allUsersRepository, Account.Id accountId)
+ throws IOException, ConfigInvalidException {
+ AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
+ accountConfig.load(allUsersRepository);
+ return accountConfig.getAccount();
+ }
+
private static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
return repo.getRefDatabase()
.getRefs(RefNames.REFS_USERS)
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 081ea26..1669c4d 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
@@ -33,6 +33,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class AccountsCollection
@@ -68,7 +70,8 @@
@Override
public AccountResource parse(TopLevelResource root, IdString id)
- throws ResourceNotFoundException, AuthException, OrmException {
+ throws ResourceNotFoundException, AuthException, OrmException, IOException,
+ ConfigInvalidException {
IdentifiedUser user = parseId(id.get());
if (user == null) {
throw new ResourceNotFoundException(id);
@@ -89,7 +92,8 @@
* account is not visible to the calling user
*/
public IdentifiedUser parse(String id)
- throws AuthException, UnprocessableEntityException, OrmException {
+ throws AuthException, UnprocessableEntityException, OrmException, IOException,
+ ConfigInvalidException {
return parseOnBehalfOf(null, id);
}
@@ -104,8 +108,11 @@
* @throws AuthException thrown if 'self' is used as account ID and the current user is not
* authenticated
* @throws OrmException
+ * @throws ConfigInvalidException
+ * @throws IOException
*/
- public IdentifiedUser parseId(String id) throws AuthException, OrmException {
+ public IdentifiedUser parseId(String id)
+ throws AuthException, OrmException, IOException, ConfigInvalidException {
return parseIdOnBehalfOf(null, id);
}
@@ -113,7 +120,8 @@
* Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
*/
public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
- throws AuthException, UnprocessableEntityException, OrmException {
+ throws AuthException, UnprocessableEntityException, OrmException, IOException,
+ ConfigInvalidException {
IdentifiedUser user = parseIdOnBehalfOf(caller, id);
if (user == null) {
throw new UnprocessableEntityException(String.format("Account Not Found: %s", id));
@@ -124,7 +132,7 @@
}
private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
- throws AuthException, OrmException {
+ throws AuthException, OrmException, IOException, ConfigInvalidException {
if (id.equals("self")) {
CurrentUser user = self.get();
if (user.isIdentifiedUser()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
index f6ed598..ef501ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -16,15 +16,21 @@
import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
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.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -32,7 +38,9 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.List;
import java.util.function.Consumer;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -46,8 +54,18 @@
/**
* Updates accounts.
*
- * <p>On updating accounts this class takes care to evict them from the account cache and thus
- * triggers reindex for them.
+ * <p>The account updates are written to both ReviewDb and NoteDb.
+ *
+ * <p>In NoteDb accounts are represented as user branches in the All-Users repository. Optionally a
+ * user branch can contain a 'account.config' file that stores account properties, such as full
+ * name, preferred email, status and the active flag. The timestamp of the first commit on a user
+ * branch denotes the registration date. The initial commit on the user branch may be empty (since
+ * having an 'account.config' is optional). See {@link AccountConfig} for details of the
+ * 'account.config' file format.
+ *
+ * <p>On updating accounts the accounts are evicted from the account cache and thus reindexed. The
+ * eviction from the account cache is done by the {@link ReindexAfterRefUpdate} class which receives
+ * the event about updating the user branch that is triggered by this class.
*/
@Singleton
public class AccountsUpdate {
@@ -60,56 +78,38 @@
@Singleton
public static class Server {
private final GitRepositoryManager repoManager;
- private final AccountCache accountCache;
+ private final GitReferenceUpdated gitRefUpdated;
private final AllUsersName allUsersName;
+ private final OutgoingEmailValidator emailValidator;
private final Provider<PersonIdent> serverIdent;
+ private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
@Inject
public Server(
GitRepositoryManager repoManager,
- AccountCache accountCache,
+ GitReferenceUpdated gitRefUpdated,
AllUsersName allUsersName,
- @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+ OutgoingEmailValidator emailValidator,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent,
+ Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory) {
this.repoManager = repoManager;
- this.accountCache = accountCache;
+ this.gitRefUpdated = gitRefUpdated;
this.allUsersName = allUsersName;
+ this.emailValidator = emailValidator;
this.serverIdent = serverIdent;
+ this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
}
public AccountsUpdate create() {
PersonIdent i = serverIdent.get();
- return new AccountsUpdate(repoManager, accountCache, allUsersName, i, i);
- }
- }
-
- /**
- * Factory to create an AccountsUpdate instance for updating accounts by the Gerrit server.
- *
- * <p>Using this class will not perform reindexing for the updated accounts and they will also not
- * be evicted from the account cache.
- *
- * <p>The Gerrit server identity will be used as author and committer for all commits that update
- * the accounts.
- */
- @Singleton
- public static class ServerNoReindex {
- private final GitRepositoryManager repoManager;
- private final AllUsersName allUsersName;
- private final Provider<PersonIdent> serverIdent;
-
- @Inject
- public ServerNoReindex(
- GitRepositoryManager repoManager,
- AllUsersName allUsersName,
- @GerritPersonIdent Provider<PersonIdent> serverIdent) {
- this.repoManager = repoManager;
- this.allUsersName = allUsersName;
- this.serverIdent = serverIdent;
- }
-
- public AccountsUpdate create() {
- PersonIdent i = serverIdent.get();
- return new AccountsUpdate(repoManager, null, allUsersName, i, i);
+ return new AccountsUpdate(
+ repoManager,
+ gitRefUpdated,
+ null,
+ allUsersName,
+ emailValidator,
+ i,
+ () -> metaDataUpdateServerFactory.get().create(allUsersName));
}
}
@@ -122,29 +122,42 @@
@Singleton
public static class User {
private final GitRepositoryManager repoManager;
- private final AccountCache accountCache;
+ private final GitReferenceUpdated gitRefUpdated;
private final AllUsersName allUsersName;
+ private final OutgoingEmailValidator emailValidator;
private final Provider<PersonIdent> serverIdent;
private final Provider<IdentifiedUser> identifiedUser;
+ private final Provider<MetaDataUpdate.User> metaDataUpdateUserFactory;
@Inject
public User(
GitRepositoryManager repoManager,
- AccountCache accountCache,
+ GitReferenceUpdated gitRefUpdated,
AllUsersName allUsersName,
+ OutgoingEmailValidator emailValidator,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
- Provider<IdentifiedUser> identifiedUser) {
+ Provider<IdentifiedUser> identifiedUser,
+ Provider<MetaDataUpdate.User> metaDataUpdateUserFactory) {
this.repoManager = repoManager;
- this.accountCache = accountCache;
+ this.gitRefUpdated = gitRefUpdated;
this.allUsersName = allUsersName;
this.serverIdent = serverIdent;
+ this.emailValidator = emailValidator;
this.identifiedUser = identifiedUser;
+ this.metaDataUpdateUserFactory = metaDataUpdateUserFactory;
}
public AccountsUpdate create() {
+ IdentifiedUser user = identifiedUser.get();
PersonIdent i = serverIdent.get();
return new AccountsUpdate(
- repoManager, accountCache, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
+ repoManager,
+ gitRefUpdated,
+ user,
+ allUsersName,
+ emailValidator,
+ createPersonIdent(i, user),
+ () -> metaDataUpdateUserFactory.get().create(allUsersName));
}
private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -153,117 +166,178 @@
}
private final GitRepositoryManager repoManager;
- @Nullable private final AccountCache accountCache;
+ private final GitReferenceUpdated gitRefUpdated;
+ @Nullable private final IdentifiedUser currentUser;
private final AllUsersName allUsersName;
+ private final OutgoingEmailValidator emailValidator;
private final PersonIdent committerIdent;
- private final PersonIdent authorIdent;
+ private final MetaDataUpdateFactory metaDataUpdateFactory;
private AccountsUpdate(
GitRepositoryManager repoManager,
- @Nullable AccountCache accountCache,
+ GitReferenceUpdated gitRefUpdated,
+ @Nullable IdentifiedUser currentUser,
AllUsersName allUsersName,
+ OutgoingEmailValidator emailValidator,
PersonIdent committerIdent,
- PersonIdent authorIdent) {
+ MetaDataUpdateFactory metaDataUpdateFactory) {
this.repoManager = checkNotNull(repoManager, "repoManager");
- this.accountCache = accountCache;
+ this.gitRefUpdated = checkNotNull(gitRefUpdated, "gitRefUpdated");
+ this.currentUser = currentUser;
this.allUsersName = checkNotNull(allUsersName, "allUsersName");
+ this.emailValidator = checkNotNull(emailValidator, "emailValidator");
this.committerIdent = checkNotNull(committerIdent, "committerIdent");
- this.authorIdent = checkNotNull(authorIdent, "authorIdent");
+ this.metaDataUpdateFactory = checkNotNull(metaDataUpdateFactory, "metaDataUpdateFactory");
}
/**
* Inserts a new account.
*
+ * @param db ReviewDb
+ * @param accountId ID of the new account
+ * @param init consumer to populate the new account
+ * @return the newly created account
+ * @throws OrmException if updating the database fails
* @throws OrmDuplicateKeyException if the account already exists
* @throws IOException if updating the user branch fails
+ * @throws ConfigInvalidException if any of the account fields has an invalid value
*/
- public void insert(ReviewDb db, Account account) throws OrmException, IOException {
+ public Account insert(ReviewDb db, Account.Id accountId, Consumer<Account> init)
+ throws OrmException, IOException, ConfigInvalidException {
+ AccountConfig accountConfig = read(accountId);
+ Account account = accountConfig.getNewAccount();
+ init.accept(account);
+
+ // Create in ReviewDb
db.accounts().insert(ImmutableSet.of(account));
- createUserBranch(account);
- evictAccount(account.getId());
- }
- /**
- * Inserts or updates an account.
- *
- * <p>If the account already exists, it is overwritten, otherwise it is inserted.
- */
- public void upsert(ReviewDb db, Account account) throws OrmException, IOException {
- db.accounts().upsert(ImmutableSet.of(account));
- createUserBranchIfNeeded(account);
- evictAccount(account.getId());
- }
-
- /** Updates the account. */
- public void update(ReviewDb db, Account account) throws OrmException, IOException {
- db.accounts().update(ImmutableSet.of(account));
- evictAccount(account.getId());
+ // Create in NoteDb
+ commitNew(accountConfig);
+ return account;
}
/**
* Gets the account and updates it atomically.
*
+ * <p>Changing the registration date of an account is not supported.
+ *
* @param db ReviewDb
* @param accountId ID of the account
* @param consumer consumer to update the account, only invoked if the account exists
* @return the updated account, {@code null} if the account doesn't exist
- * @throws OrmException if updating the account fails
+ * @throws OrmException if updating the database fails
+ * @throws IOException if updating the user branch fails
+ * @throws ConfigInvalidException if any of the account fields has an invalid value
*/
- public Account atomicUpdate(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
- throws OrmException, IOException {
- Account account =
- db.accounts()
- .atomicUpdate(
- accountId,
- a -> {
- consumer.accept(a);
- return a;
- });
- evictAccount(accountId);
+ public Account update(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
+ throws OrmException, IOException, ConfigInvalidException {
+ return update(db, accountId, ImmutableList.of(consumer));
+ }
+
+ /**
+ * Gets the account and updates it atomically.
+ *
+ * <p>Changing the registration date of an account is not supported.
+ *
+ * @param db ReviewDb
+ * @param accountId ID of the account
+ * @param consumers consumers to update the account, only invoked if the account exists
+ * @return the updated account, {@code null} if the account doesn't exist
+ * @throws OrmException if updating the database fails
+ * @throws IOException if updating the user branch fails
+ * @throws ConfigInvalidException if any of the account fields has an invalid value
+ */
+ public Account update(ReviewDb db, Account.Id accountId, List<Consumer<Account>> consumers)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (consumers.isEmpty()) {
+ return null;
+ }
+
+ // Update in ReviewDb
+ db.accounts()
+ .atomicUpdate(
+ accountId,
+ a -> {
+ consumers.stream().forEach(c -> c.accept(a));
+ return a;
+ });
+
+ // Update in NoteDb
+ AccountConfig accountConfig = read(accountId);
+ Account account = accountConfig.getAccount();
+ consumers.stream().forEach(c -> c.accept(account));
+ commit(accountConfig);
+
return account;
}
- /** Deletes the account. */
+ /**
+ * Replaces the account.
+ *
+ * <p>The existing account with the same account ID is overwritten by the given account. Choosing
+ * to overwrite an account means that any updates that were done to the account by a racing
+ * request after the account was read are lost. Updates are also lost if the account was read from
+ * a stale account index. This is why using {@link #update(ReviewDb,
+ * com.google.gerrit.reviewdb.client.Account.Id, Consumer)} to do an atomic update is always
+ * preferred.
+ *
+ * <p>Changing the registration date of an account is not supported.
+ *
+ * @param db ReviewDb
+ * @param account the new account
+ * @throws OrmException if updating the database fails
+ * @throws IOException if updating the user branch fails
+ * @throws ConfigInvalidException if any of the account fields has an invalid value
+ * @see #update(ReviewDb, com.google.gerrit.reviewdb.client.Account.Id, Consumer)
+ */
+ public void replace(ReviewDb db, Account account)
+ throws OrmException, IOException, ConfigInvalidException {
+ // Update in ReviewDb
+ db.accounts().update(ImmutableSet.of(account));
+
+ // Update in NoteDb
+ AccountConfig accountConfig = read(account.getId());
+ accountConfig.setAccount(account);
+ commit(accountConfig);
+ }
+
+ /**
+ * Deletes the account.
+ *
+ * @param db ReviewDb
+ * @param account the account that should be deleted
+ * @throws OrmException if updating the database fails
+ * @throws IOException if updating the user branch fails
+ */
public void delete(ReviewDb db, Account account) throws OrmException, IOException {
+ // Delete in ReviewDb
db.accounts().delete(ImmutableSet.of(account));
+
+ // Delete in NoteDb
deleteUserBranch(account.getId());
- evictAccount(account.getId());
}
- /** Deletes the account. */
+ /**
+ * Deletes the account.
+ *
+ * @param db ReviewDb
+ * @param accountId the ID of the account that should be deleted
+ * @throws OrmException if updating the database fails
+ * @throws IOException if updating the user branch fails
+ */
public void deleteByKey(ReviewDb db, Account.Id accountId) throws OrmException, IOException {
+ // Delete in ReviewDb
db.accounts().deleteKeys(ImmutableSet.of(accountId));
+
+ // Delete in NoteDb
deleteUserBranch(accountId);
- evictAccount(accountId);
- }
-
- private void createUserBranch(Account account) throws IOException {
- try (Repository repo = repoManager.openRepository(allUsersName);
- ObjectInserter oi = repo.newObjectInserter()) {
- String refName = RefNames.refsUsers(account.getId());
- if (repo.exactRef(refName) != null) {
- throw new IOException(
- String.format(
- "User branch %s for newly created account %s already exists.",
- refName, account.getId().get()));
- }
- createUserBranch(
- repo, oi, committerIdent, authorIdent, account.getId(), account.getRegisteredOn());
- }
- }
-
- private void createUserBranchIfNeeded(Account account) throws IOException {
- try (Repository repo = repoManager.openRepository(allUsersName);
- ObjectInserter oi = repo.newObjectInserter()) {
- if (repo.exactRef(RefNames.refsUsers(account.getId())) == null) {
- createUserBranch(
- repo, oi, committerIdent, authorIdent, account.getId(), account.getRegisteredOn());
- }
- }
}
public static void createUserBranch(
Repository repo,
+ Project.NameKey project,
+ GitReferenceUpdated gitRefUpdated,
+ @Nullable IdentifiedUser user,
ObjectInserter oi,
PersonIdent committerIdent,
PersonIdent authorIdent,
@@ -283,6 +357,7 @@
if (result != Result.NEW) {
throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
}
+ gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null);
}
private static ObjectId createInitialEmptyCommit(
@@ -307,12 +382,18 @@
private void deleteUserBranch(Account.Id accountId) throws IOException {
try (Repository repo = repoManager.openRepository(allUsersName)) {
- deleteUserBranch(repo, committerIdent, accountId);
+ deleteUserBranch(repo, allUsersName, gitRefUpdated, currentUser, committerIdent, accountId);
}
}
public static void deleteUserBranch(
- Repository repo, PersonIdent refLogIdent, Account.Id accountId) throws IOException {
+ Repository repo,
+ Project.NameKey project,
+ GitReferenceUpdated gitRefUpdated,
+ @Nullable IdentifiedUser user,
+ PersonIdent refLogIdent,
+ Account.Id accountId)
+ throws IOException {
String refName = RefNames.refsUsers(accountId);
Ref ref = repo.exactRef(refName);
if (ref == null) {
@@ -329,11 +410,37 @@
if (result != Result.FORCED) {
throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
}
+ gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null);
}
- private void evictAccount(Account.Id accountId) throws IOException {
- if (accountCache != null) {
- accountCache.evict(accountId);
+ private AccountConfig read(Account.Id accountId) throws IOException, ConfigInvalidException {
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
+ accountConfig.load(repo);
+ return accountConfig;
}
}
+
+ private void commitNew(AccountConfig accountConfig) throws IOException {
+ // When creating a new account we must allow empty commits so that the user branch gets created
+ // with an empty commit when no account properties are set and hence no 'account.config' file
+ // will be created.
+ commit(accountConfig, true);
+ }
+
+ private void commit(AccountConfig accountConfig) throws IOException {
+ commit(accountConfig, false);
+ }
+
+ private void commit(AccountConfig accountConfig, boolean allowEmptyCommit) throws IOException {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create()) {
+ md.setAllowEmpty(allowEmptyCommit);
+ accountConfig.commit(md);
+ }
+ }
+
+ @FunctionalInterface
+ private static interface MetaDataUpdateFactory {
+ MetaDataUpdate create() throws IOException;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index d617365..ff367e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -17,7 +17,7 @@
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -113,12 +113,15 @@
}
@Override
- public Response<AccountInfo> apply(TopLevelResource rsrc, AccountInput input)
+ public Response<AccountInfo> apply(TopLevelResource rsrc, @Nullable AccountInput input)
throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
OrmException, IOException, ConfigInvalidException {
- if (input == null) {
- input = new AccountInput();
- }
+ return apply(input != null ? input : new AccountInput());
+ }
+
+ public Response<AccountInfo> apply(AccountInput input)
+ throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
+ OrmException, IOException, ConfigInvalidException {
if (input.username != null && !username.equals(input.username)) {
throw new BadRequestException("username must match URL");
}
@@ -171,10 +174,15 @@
}
}
- Account a = new Account(id, TimeUtil.nowTs());
- a.setFullName(input.name);
- a.setPreferredEmail(input.email);
- accountsUpdate.create().insert(db, a);
+ accountsUpdate
+ .create()
+ .insert(
+ db,
+ id,
+ a -> {
+ a.setFullName(input.name);
+ a.setPreferredEmail(input.email);
+ });
for (AccountGroup.Id groupId : groups) {
AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
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 3a996f2..1fa8ed3 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
@@ -31,6 +31,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
@Singleton
@@ -53,7 +54,7 @@
@Override
public Response<?> apply(AccountResource rsrc, Input input)
- throws RestApiException, OrmException, IOException {
+ throws RestApiException, OrmException, IOException, ConfigInvalidException {
if (self.get() == rsrc.getUser()) {
throw new ResourceConflictException("cannot deactivate own account");
}
@@ -62,7 +63,7 @@
Account account =
accountsUpdate
.create()
- .atomicUpdate(
+ .update(
dbProvider.get(),
rsrc.getUser().getAccountId(),
a -> {
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 bc7c1d0..6051a95 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
@@ -28,6 +28,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
@Singleton
@@ -45,12 +46,12 @@
@Override
public Response<String> apply(AccountResource rsrc, Input input)
- throws ResourceNotFoundException, OrmException, IOException {
+ throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
AtomicBoolean alreadyActive = new AtomicBoolean(false);
Account account =
accountsUpdate
.create()
- .atomicUpdate(
+ .update(
dbProvider.get(),
rsrc.getUser().getAccountId(),
a -> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 0911820..792e71d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -35,6 +35,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutName implements RestModifyView<AccountResource, Input> {
@@ -65,7 +66,7 @@
@Override
public Response<String> apply(AccountResource rsrc, Input input)
throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
- IOException, PermissionBackendException {
+ IOException, PermissionBackendException, ConfigInvalidException {
if (self.get() != rsrc.getUser()) {
permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
}
@@ -73,7 +74,8 @@
}
public Response<String> apply(IdentifiedUser user, Input input)
- throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException {
+ throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException,
+ ConfigInvalidException {
if (input == null) {
input = new Input();
}
@@ -86,7 +88,7 @@
Account account =
accountsUpdate
.create()
- .atomicUpdate(dbProvider.get(), user.getAccountId(), a -> a.setFullName(newName));
+ .update(dbProvider.get(), user.getAccountId(), a -> a.setFullName(newName));
if (account == null) {
throw new ResourceNotFoundException("account not found");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index d473d53..98d4ac5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -32,6 +32,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutPreferred implements RestModifyView<AccountResource.Email, Input> {
@@ -57,7 +58,7 @@
@Override
public Response<String> apply(AccountResource.Email rsrc, Input input)
throws AuthException, ResourceNotFoundException, OrmException, IOException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
if (self.get() != rsrc.getUser()) {
permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
}
@@ -65,12 +66,12 @@
}
public Response<String> apply(IdentifiedUser user, String email)
- throws ResourceNotFoundException, OrmException, IOException {
+ throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
Account account =
accountsUpdate
.create()
- .atomicUpdate(
+ .update(
dbProvider.get(),
user.getAccountId(),
a -> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
index 81c0694..136fc68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
@@ -33,6 +33,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutStatus implements RestModifyView<AccountResource, Input> {
@@ -66,7 +67,7 @@
@Override
public Response<String> apply(AccountResource rsrc, Input input)
throws AuthException, ResourceNotFoundException, OrmException, IOException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
if (self.get() != rsrc.getUser()) {
permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
}
@@ -74,7 +75,7 @@
}
public Response<String> apply(IdentifiedUser user, Input input)
- throws ResourceNotFoundException, OrmException, IOException {
+ throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
if (input == null) {
input = new Input();
}
@@ -83,7 +84,7 @@
Account account =
accountsUpdate
.create()
- .atomicUpdate(
+ .update(
dbProvider.get(),
user.getAccountId(),
a -> a.setStatus(Strings.nullToEmpty(newStatus)));
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 7562801..ce31cac 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
@@ -27,6 +27,7 @@
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.OptionDef;
@@ -82,8 +83,12 @@
throw new CmdLineException(owner, "user \"" + token + "\" not found");
}
}
- } catch (OrmException | IOException e) {
+ } catch (OrmException e) {
throw new CmdLineException(owner, "database is down");
+ } catch (IOException e) {
+ throw new CmdLineException(owner, "Failed to load account", e);
+ } catch (ConfigInvalidException e) {
+ throw new CmdLineException(owner, "Invalid account config", e);
}
setter.addValue(accountId);
return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index 7bf4214..5073e4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -41,7 +41,9 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
@@ -68,7 +70,8 @@
@Override
protected ChangeInfo applyImpl(
BatchUpdate.Factory updateFactory, ChangeResource req, AbandonInput input)
- throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+ throws RestApiException, UpdateException, OrmException, PermissionBackendException,
+ IOException, ConfigInvalidException {
req.permissions().database(dbProvider).check(ChangePermission.ABANDON);
NotifyHandling notify = input.notify == null ? defaultNotify(req.getControl()) : input.notify;
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 b4867c4..993148e 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
@@ -39,6 +39,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class CherryPick
@@ -67,7 +68,7 @@
public ChangeInfo applyImpl(
BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
throws OrmException, IOException, UpdateException, RestApiException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
input.parent = input.parent == null ? 1 : input.parent;
if (input.message == null || input.message.trim().isEmpty()) {
throw new BadRequestException("message must be non-empty");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 755379d..fbb692c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -62,6 +62,7 @@
import java.sql.Timestamp;
import java.util.List;
import java.util.TimeZone;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -124,7 +125,7 @@
CherryPickInput input,
RefControl refControl)
throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
- UpdateException, RestApiException {
+ UpdateException, RestApiException, ConfigInvalidException {
return cherryPick(
batchUpdateFactory,
change.getId(),
@@ -148,7 +149,7 @@
CherryPickInput input,
RefControl destRefControl)
throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
- UpdateException, RestApiException {
+ UpdateException, RestApiException, ConfigInvalidException {
IdentifiedUser identifiedUser = user.get();
try (Repository git = gitManager.openRepository(project);
@@ -320,7 +321,7 @@
ChangeControl destCtl,
CodeReviewCommit cherryPickCommit,
CherryPickInput input)
- throws IOException, OrmException, BadRequestException {
+ throws IOException, OrmException, BadRequestException, ConfigInvalidException {
Change destChange = destCtl.getChange();
PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
PatchSet current = psUtil.current(dbProvider.get(), destCtl.getNotes());
@@ -343,7 +344,7 @@
Branch.NameKey sourceBranch,
ObjectId sourceCommit,
CherryPickInput input)
- throws OrmException, IOException, BadRequestException {
+ throws OrmException, IOException, BadRequestException, ConfigInvalidException {
Change.Id changeId = new Change.Id(seq.nextChangeId());
ChangeInserter ins =
changeInserterFactory.create(changeId, cherryPickCommit, refName).setTopic(topic);
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 41f0463..ac70e4a 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
@@ -39,6 +39,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.revwalk.RevCommit;
@Singleton
@@ -67,7 +68,7 @@
public ChangeInfo applyImpl(
BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
throws OrmException, IOException, UpdateException, RestApiException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
RevCommit commit = rsrc.getCommit();
String message = Strings.nullToEmpty(input.message).trim();
input.message = message.isEmpty() ? commit.getFullMessage() : message;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index d0e489b..9fcb13d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -68,6 +68,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -226,7 +227,7 @@
if (accounts.get(db.get(), change().getOwner()) == null) {
problem("Missing change owner: " + change().getOwner());
}
- } catch (OrmException e) {
+ } catch (OrmException | IOException | ConfigInvalidException e) {
error("Failed to look up owner", e);
}
}
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 cca9cb6..bbe04f5 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
@@ -74,6 +74,7 @@
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -151,7 +152,7 @@
protected Response<ChangeInfo> applyImpl(
BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
- UpdateException, PermissionBackendException {
+ UpdateException, PermissionBackendException, ConfigInvalidException {
if (Strings.isNullOrEmpty(input.project)) {
throw new BadRequestException("project must be non-empty");
}
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 8516615..ccc7587 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
@@ -31,10 +31,12 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class NotifyUtil {
@@ -76,7 +78,7 @@
public ListMultimap<RecipientType, Account.Id> resolveAccounts(
@Nullable Map<RecipientType, NotifyInfo> notifyDetails)
- throws OrmException, BadRequestException {
+ throws OrmException, BadRequestException, IOException, ConfigInvalidException {
if (isNullOrEmpty(notifyDetails)) {
return ImmutableListMultimap.of();
}
@@ -96,7 +98,7 @@
}
private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails)
- throws OrmException, BadRequestException {
+ throws OrmException, BadRequestException, IOException, ConfigInvalidException {
List<String> missing = new ArrayList<>(nameOrEmails.size());
List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
for (String nameOrEmail : nameOrEmails) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index d6ad28b..f5a1856 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -126,6 +126,7 @@
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -190,14 +191,14 @@
protected Response<ReviewResult> applyImpl(
BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input)
throws RestApiException, UpdateException, OrmException, IOException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
return apply(updateFactory, revision, input, TimeUtil.nowTs());
}
public Response<ReviewResult> apply(
BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts)
throws RestApiException, UpdateException, OrmException, IOException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
// Respect timestamp, but truncate at change created-on time.
ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
if (revision.getEdit().isPresent()) {
@@ -367,7 +368,7 @@
private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
throws BadRequestException, AuthException, UnprocessableEntityException, OrmException,
- PermissionBackendException {
+ PermissionBackendException, IOException, ConfigInvalidException {
if (in.labels == null || in.labels.isEmpty()) {
throw new AuthException(
String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index c7b0031..a7711b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -76,6 +76,7 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -148,7 +149,7 @@
protected AddReviewerResult applyImpl(
BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input)
throws IOException, OrmException, RestApiException, UpdateException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
if (input.reviewer == null) {
throw new BadRequestException("missing reviewer field");
}
@@ -170,7 +171,7 @@
public Addition prepareApplication(
ChangeResource rsrc, AddReviewerInput input, boolean allowGroup)
- throws OrmException, IOException, PermissionBackendException {
+ throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
String reviewer = input.reviewer;
ReviewerState state = input.state();
NotifyHandling notify = input.notify;
@@ -219,7 +220,7 @@
ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean allowGroup,
boolean allowByEmail)
- throws OrmException, PermissionBackendException {
+ throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
Account.Id accountId = null;
try {
accountId = accounts.parse(reviewer).getAccountId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 53f127e..9ef445d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -45,6 +45,7 @@
import java.io.OutputStream;
import java.util.Collection;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
@@ -82,7 +83,7 @@
@Override
public BinaryResult apply(RevisionResource rsrc)
- throws OrmException, RestApiException, UpdateException {
+ throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
if (Strings.isNullOrEmpty(format)) {
throw new BadRequestException("format is not specified");
}
@@ -110,7 +111,7 @@
}
private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
- throws OrmException, RestApiException, UpdateException {
+ throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
ReviewDb db = dbProvider.get();
ChangeControl control = rsrc.getControl();
IdentifiedUser caller = control.getUser().asIdentifiedUser();
@@ -125,7 +126,12 @@
.setContentType(f.getMimeType())
.setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
return bin;
- } catch (OrmException | RestApiException | UpdateException | RuntimeException e) {
+ } catch (OrmException
+ | RestApiException
+ | UpdateException
+ | IOException
+ | ConfigInvalidException
+ | RuntimeException e) {
op.close();
throw e;
}
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 eab06fb..cbb5fa3 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
@@ -35,6 +35,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PublishChangeEdit
@@ -85,7 +86,8 @@
@Override
protected Response<?> applyImpl(
BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
- throws IOException, OrmException, RestApiException, UpdateException {
+ throws IOException, OrmException, RestApiException, UpdateException,
+ ConfigInvalidException {
CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (!edit.isPresent()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index b07d24b..a0862d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -42,6 +42,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo>
@@ -73,7 +74,7 @@
protected AccountInfo applyImpl(
BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input)
throws RestApiException, UpdateException, OrmException, IOException,
- PermissionBackendException {
+ PermissionBackendException, ConfigInvalidException {
rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
input.assignee = Strings.nullToEmpty(input.assignee).trim();
@@ -109,7 +110,7 @@
}
private Addition addAssigneeAsCC(ChangeResource rsrc, String assignee)
- throws OrmException, IOException, PermissionBackendException {
+ throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
AddReviewerInput reviewerInput = new AddReviewerInput();
reviewerInput.reviewer = assignee;
reviewerInput.state = ReviewerState.CC;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
index dcb5766..95e29ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -51,6 +51,7 @@
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -107,7 +108,7 @@
protected Response<String> applyImpl(
BatchUpdate.Factory updateFactory, ChangeResource resource, Input input)
throws IOException, UnchangedCommitMessageException, RestApiException, UpdateException,
- PermissionBackendException, OrmException {
+ PermissionBackendException, OrmException, ConfigInvalidException {
PatchSet ps = psUtil.current(db.get(), resource.getNotes());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
@@ -150,6 +151,7 @@
psInserterFactory.create(resource.getControl(), psId, newCommit);
inserter.setMessage(
String.format("Patch Set %s: Commit message was updated.", psId.getId()));
+ inserter.setDescription("Edit commit message");
inserter.setNotify(input.notify);
inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
bu.addOp(resource.getChange().getId(), inserter);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
index 0762f0e..8794083 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -30,7 +30,9 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class Reviewers implements ChildCollection<ChangeResource, ReviewerResource> {
@@ -69,7 +71,8 @@
@Override
public ReviewerResource parse(ChangeResource rsrc, IdString id)
- throws OrmException, ResourceNotFoundException, AuthException {
+ throws OrmException, ResourceNotFoundException, AuthException, IOException,
+ ConfigInvalidException {
Address address = Address.tryParse(id.get());
Account.Id accountId = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
index 2dc7ad8..be8bce0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
@@ -31,7 +31,9 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class RevisionReviewers implements ChildCollection<RevisionResource, ReviewerResource> {
@@ -70,7 +72,8 @@
@Override
public ReviewerResource parse(RevisionResource rsrc, IdString id)
- throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException {
+ throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException,
+ IOException, ConfigInvalidException {
if (!rsrc.isCurrent()) {
throw new MethodNotAllowedException("Cannot access on non-current patch set");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 3fb8a79..3dd467a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -73,6 +73,7 @@
import java.util.Map;
import java.util.Queue;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -202,7 +203,7 @@
@Override
public Output apply(RevisionResource rsrc, SubmitInput input)
throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
- PermissionBackendException, UpdateException {
+ PermissionBackendException, UpdateException, ConfigInvalidException {
input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
IdentifiedUser submitter;
if (input.onBehalfOf != null) {
@@ -216,7 +217,7 @@
}
public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
- throws OrmException, RestApiException, IOException, UpdateException {
+ throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException {
Change change = rsrc.getChange();
if (!change.getStatus().isOpen()) {
throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -249,7 +250,7 @@
if (msg != null) {
throw new ResourceConflictException(msg.getMessage());
}
- //$FALL-THROUGH$
+ // $FALL-THROUGH$
case ABANDONED:
case DRAFT:
default:
@@ -478,7 +479,8 @@
}
private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
- throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException {
+ throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException,
+ IOException, ConfigInvalidException {
PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider);
perm.check(ChangePermission.SUBMIT);
perm.check(ChangePermission.SUBMIT_AS);
@@ -527,7 +529,7 @@
@Override
public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
- PermissionBackendException, UpdateException {
+ PermissionBackendException, UpdateException, ConfigInvalidException {
PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 1daa7e3..178aeea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -34,6 +34,7 @@
import com.google.inject.Provider;
import java.io.IOException;
import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;
@@ -66,7 +67,7 @@
@Override
public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
- throws AuthException, BadRequestException, OrmException, IOException {
+ throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException {
if (!self.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
index 84db266..c89684c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
@@ -39,6 +39,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class CheckAccess implements RestModifyView<ConfigResource, AccessCheckInput> {
@@ -67,7 +68,8 @@
@Override
public AccessCheckInfo apply(ConfigResource unused, AccessCheckInput input)
- throws OrmException, PermissionBackendException, RestApiException, IOException {
+ throws OrmException, PermissionBackendException, RestApiException, IOException,
+ ConfigInvalidException {
permissionBackend.user(currentUser.get()).check(GlobalPermission.ADMINISTRATE_SERVER);
if (input == null) {
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 6bd7c57..8612737 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
@@ -126,6 +126,7 @@
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.MergeValidationListener;
import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.git.validators.MergeValidators.AccountValidator;
import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.git.validators.OnSubmitValidators;
@@ -389,6 +390,7 @@
bind(AnonymousUser.class);
factory(AbandonOp.Factory.class);
+ factory(AccountValidator.Factory.class);
factory(RefOperationValidators.Factory.class);
factory(OnSubmitValidators.Factory.class);
factory(MergeValidators.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
index 2ac6695..674a5c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
@@ -159,6 +159,7 @@
}
public void setGroupReference(String name, GroupReference value) {
- setString(name, value.toConfigValue());
+ GroupReference groupRef = projectConfig.resolve(value);
+ setString(name, groupRef.toConfigValue());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index ce6b5c6..353cba2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -88,6 +88,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -430,7 +431,7 @@
boolean checkSubmitRules,
SubmitInput submitInput,
boolean dryrun)
- throws OrmException, RestApiException, UpdateException {
+ throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
this.submitInput = submitInput;
this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
this.dryrun = dryrun;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 0c73d05..91379fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -416,7 +416,13 @@
}
public GroupReference resolve(GroupReference group) {
- return groupList.resolve(group);
+ GroupReference groupRef = groupList.resolve(group);
+ if (groupRef != null
+ && groupRef.getUUID() != null
+ && !groupsByName.containsKey(groupRef.getName())) {
+ groupsByName.put(groupRef.getName(), groupRef);
+ }
+ return groupRef;
}
/** @return the group reference, if the group is used by at least one rule. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 15a0935..6b3f173 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -84,7 +84,6 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.SetHashtagsOp;
@@ -155,6 +154,7 @@
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
@@ -301,7 +301,6 @@
private final Sequences seq;
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeNotes.Factory notesFactory;
- private final Accounts accounts;
private final AccountsUpdate.Server accountsUpdate;
private final AccountResolver accountResolver;
private final PermissionBackend permissionBackend;
@@ -376,7 +375,6 @@
Sequences seq,
Provider<InternalChangeQuery> queryProvider,
ChangeNotes.Factory notesFactory,
- Accounts accounts,
AccountsUpdate.Server accountsUpdate,
AccountResolver accountResolver,
PermissionBackend permissionBackend,
@@ -416,7 +414,6 @@
this.seq = seq;
this.queryProvider = queryProvider;
this.notesFactory = notesFactory;
- this.accounts = accounts;
this.accountsUpdate = accountsUpdate;
this.accountResolver = accountResolver;
this.permissionBackend = permissionBackend;
@@ -815,7 +812,11 @@
} catch (ResourceConflictException e) {
addMessage(e.getMessage());
reject(magicBranchCmd, "conflict");
- } catch (RestApiException | OrmException | UpdateException e) {
+ } catch (RestApiException
+ | OrmException
+ | UpdateException
+ | IOException
+ | ConfigInvalidException e) {
logError("Error submitting changes to " + project.getName(), e);
reject(magicBranchCmd, "error during submit");
}
@@ -1070,15 +1071,19 @@
}
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
- if (ctl.canCreate(rp.getRepository(), obj)) {
- if (!validRefOperation(cmd)) {
- return;
- }
- validateNewCommits(ctl, cmd);
- actualCommands.add(cmd);
- } else {
- reject(cmd, "prohibited by Gerrit: create access denied for " + cmd.getRefName());
+ String rejectReason = ctl.canCreate(rp.getRepository(), obj);
+ if (rejectReason != null) {
+ reject(cmd, "prohibited by Gerrit: " + rejectReason);
+ return;
}
+
+ if (!validRefOperation(cmd)) {
+ // validRefOperation sets messages, so no need to provide more feedback.
+ return;
+ }
+
+ validateNewCommits(ctl, cmd);
+ actualCommands.add(cmd);
}
private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
@@ -2254,7 +2259,7 @@
}
private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
- throws OrmException, RestApiException, UpdateException {
+ throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
for (CreateRequest r : create) {
checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId);
@@ -2779,13 +2784,22 @@
if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
try {
- Account a = accounts.get(db, user.getAccountId());
- if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
- a.setFullName(c.getCommitterIdent().getName());
- accountsUpdate.create().update(db, a);
- user.getAccount().setFullName(a.getFullName());
+ String committerName = c.getCommitterIdent().getName();
+ Account account =
+ accountsUpdate
+ .create()
+ .update(
+ db,
+ user.getAccountId(),
+ a -> {
+ if (Strings.isNullOrEmpty(a.getFullName())) {
+ a.setFullName(committerName);
+ }
+ });
+ if (account != null && Strings.isNullOrEmpty(account.getFullName())) {
+ user.getAccount().setFullName(account.getFullName());
}
- } catch (OrmException e) {
+ } catch (OrmException | IOException | ConfigInvalidException e) {
logWarn("Cannot default full_name", e);
} finally {
defaultName = false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
index 12ad776..76f1369 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -25,7 +25,6 @@
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index 6b6d97f..8b7df19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -75,6 +75,7 @@
}
protected RevCommit revision;
+ protected RevWalk rw;
protected ObjectReader reader;
protected ObjectInserter inserter;
protected DirCache newTree;
@@ -153,11 +154,13 @@
* @throws ConfigInvalidException
*/
public void load(RevWalk walk, ObjectId id) throws IOException, ConfigInvalidException {
+ this.rw = walk;
this.reader = walk.getObjectReader();
try {
revision = id != null ? walk.parseCommit(id) : null;
onLoad();
} finally {
+ walk = null;
reader = null;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index ff755ea..bdb0db9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -31,6 +31,7 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountConfig;
import com.google.gerrit.server.account.WatchConfig;
import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.gerrit.server.config.AllUsersName;
@@ -57,9 +58,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
@@ -67,6 +70,7 @@
import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -120,7 +124,8 @@
new ConfigValidator(refctl, rw, allUsers),
new BannedCommitsValidator(rejectCommits),
new PluginCommitValidationListener(pluginValidators),
- new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker)));
+ new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
+ new AccountValidator(allUsers)));
}
public CommitValidators forGerritCommits(
@@ -135,7 +140,8 @@
new ChangeIdValidator(refctl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
new ConfigValidator(refctl, rw, allUsers),
new PluginCommitValidationListener(pluginValidators),
- new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker)));
+ new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
+ new AccountValidator(allUsers)));
}
public CommitValidators forMergedCommits(PermissionBackend.ForRef perm, RefControl refControl) {
@@ -679,6 +685,60 @@
}
}
+ /** Rejects updates to 'account.config' in user branches. */
+ public static class AccountValidator implements CommitValidationListener {
+ private final AllUsersName allUsers;
+
+ public AccountValidator(AllUsersName allUsers) {
+ this.allUsers = allUsers;
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ if (!allUsers.equals(receiveEvent.project.getNameKey())) {
+ return Collections.emptyList();
+ }
+
+ if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) {
+ // no validation on push for review, will be checked on submit by
+ // MergeValidators.AccountValidator
+ return Collections.emptyList();
+ }
+
+ Account.Id accountId = Account.Id.fromRef(receiveEvent.refName);
+ if (accountId == null) {
+ return Collections.emptyList();
+ }
+
+ try {
+ ObjectId newBlobId = getAccountConfigBlobId(receiveEvent.revWalk, receiveEvent.commit);
+
+ ObjectId oldId = receiveEvent.command.getOldId();
+ ObjectId oldBlobId =
+ !ObjectId.zeroId().equals(oldId)
+ ? getAccountConfigBlobId(receiveEvent.revWalk, oldId)
+ : null;
+ if (!Objects.equals(oldBlobId, newBlobId)) {
+ throw new CommitValidationException("account update not allowed");
+ }
+ } catch (IOException e) {
+ String m = String.format("Validating update for account %s failed", accountId.get());
+ log.error(m, e);
+ throw new CommitValidationException(m, e);
+ }
+ return Collections.emptyList();
+ }
+
+ private ObjectId getAccountConfigBlobId(RevWalk rw, ObjectId id) throws IOException {
+ RevCommit commit = rw.parseCommit(id);
+ try (TreeWalk tw =
+ TreeWalk.forPath(rw.getObjectReader(), AccountConfig.ACCOUNT_CONFIG, commit.getTree())) {
+ return tw != null ? tw.getObjectId(0) : null;
+ }
+ }
+ }
+
private static CommitValidationMessage invalidEmail(
RevCommit c,
String type,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 298c650..af9f6d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -20,12 +20,16 @@
import com.google.gerrit.extensions.registration.DynamicMap.Entry;
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.Branch;
import com.google.gerrit.reviewdb.client.PatchSet;
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.IdentifiedUser;
+import com.google.gerrit.server.account.AccountConfig;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.CodeReviewCommit;
@@ -35,7 +39,10 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+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 java.io.IOException;
import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -48,6 +55,7 @@
private final DynamicSet<MergeValidationListener> mergeValidationListeners;
private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
+ private final AccountValidator.Factory accountValidatorFactory;
public interface Factory {
MergeValidators create();
@@ -56,9 +64,11 @@
@Inject
MergeValidators(
DynamicSet<MergeValidationListener> mergeValidationListeners,
- ProjectConfigValidator.Factory projectConfigValidatorFactory) {
+ ProjectConfigValidator.Factory projectConfigValidatorFactory,
+ AccountValidator.Factory accountValidatorFactory) {
this.mergeValidationListeners = mergeValidationListeners;
this.projectConfigValidatorFactory = projectConfigValidatorFactory;
+ this.accountValidatorFactory = accountValidatorFactory;
}
public void validatePreMerge(
@@ -72,7 +82,8 @@
List<MergeValidationListener> validators =
ImmutableList.of(
new PluginMergeValidationListener(mergeValidationListeners),
- projectConfigValidatorFactory.create());
+ projectConfigValidatorFactory.create(),
+ accountValidatorFactory.create());
for (MergeValidationListener validator : validators) {
validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId, caller);
@@ -210,4 +221,59 @@
}
}
}
+
+ public static class AccountValidator implements MergeValidationListener {
+ public interface Factory {
+ AccountValidator create();
+ }
+
+ private final Provider<ReviewDb> dbProvider;
+ private final AllUsersName allUsersName;
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ public AccountValidator(
+ Provider<ReviewDb> dbProvider,
+ AllUsersName allUsersName,
+ ChangeData.Factory changeDataFactory) {
+ this.dbProvider = dbProvider;
+ this.allUsersName = allUsersName;
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ @Override
+ public void onPreMerge(
+ Repository repo,
+ CodeReviewCommit commit,
+ ProjectState destProject,
+ Branch.NameKey destBranch,
+ PatchSet.Id patchSetId,
+ IdentifiedUser caller)
+ throws MergeValidationException {
+ if (!allUsersName.equals(destProject.getProject().getNameKey())
+ || Account.Id.fromRef(destBranch.get()) == null) {
+ return;
+ }
+
+ if (commit.getParentCount() > 1) {
+ // for merge commits we cannot ensure that the 'account.config' file is not modified, since
+ // for merge commits file modifications that come in through the merge don't appear in the
+ // file list that is returned by ChangeData#currentFilePaths()
+ throw new MergeValidationException("cannot submit merge commit to user branch");
+ }
+
+ ChangeData cd =
+ changeDataFactory.create(
+ dbProvider.get(), destProject.getProject().getNameKey(), patchSetId.getParentKey());
+ try {
+ if (cd.currentFilePaths().contains(AccountConfig.ACCOUNT_CONFIG)) {
+ throw new MergeValidationException(
+ String.format("update of %s not allowed", AccountConfig.ACCOUNT_CONFIG));
+ }
+ } catch (OrmException e) {
+ log.error("Cannot validate account update", e);
+ throw new MergeValidationException("account validation unavailable");
+ }
+ }
+ }
}
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 5c1a292..04be41e 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
@@ -52,6 +52,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class AddMembers implements RestModifyView<GroupResource, Input> {
@@ -115,7 +116,7 @@
@Override
public List<AccountInfo> apply(GroupResource resource, Input input)
throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
- IOException {
+ IOException, ConfigInvalidException {
AccountGroup internalGroup = resource.toAccountGroup();
if (internalGroup == null) {
throw new MethodNotAllowedException();
@@ -143,7 +144,8 @@
}
Account findAccount(String nameOrEmailOrId)
- throws AuthException, UnprocessableEntityException, OrmException, IOException {
+ throws AuthException, UnprocessableEntityException, OrmException, IOException,
+ ConfigInvalidException {
try {
return accounts.parse(nameOrEmailOrId).getAccount();
} catch (UnprocessableEntityException e) {
@@ -235,7 +237,7 @@
@Override
public AccountInfo apply(GroupResource resource, PutMember.Input input)
throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
- IOException {
+ IOException, ConfigInvalidException {
AddMembers.Input in = new AddMembers.Input();
in._oneMember = id;
try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index d692e59..af92acf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -55,6 +55,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
@@ -115,7 +116,7 @@
@Override
public GroupInfo apply(TopLevelResource resource, GroupInput input)
throws AuthException, BadRequestException, UnprocessableEntityException,
- ResourceConflictException, OrmException, IOException {
+ ResourceConflictException, OrmException, IOException, ConfigInvalidException {
if (input == null) {
input = new GroupInput();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index d9b1c3d..6be46d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -38,6 +38,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class DeleteMembers implements RestModifyView<GroupResource, Input> {
@@ -64,7 +65,7 @@
@Override
public Response<?> apply(GroupResource resource, Input input)
throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
- IOException {
+ IOException, ConfigInvalidException {
AccountGroup internalGroup = resource.toAccountGroup();
if (internalGroup == null) {
throw new MethodNotAllowedException();
@@ -125,7 +126,7 @@
@Override
public Response<?> apply(MemberResource resource, Input input)
throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
- IOException {
+ IOException, ConfigInvalidException {
AddMembers.Input in = new AddMembers.Input();
in._oneMember = resource.getMember().getAccountId().toString();
return delete.get().apply(resource, in);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
index 8f4d65e..dbc0676 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
@@ -32,6 +32,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class MembersCollection
@@ -63,7 +65,8 @@
@Override
public MemberResource parse(GroupResource parent, IdString id)
- throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException {
+ throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException,
+ IOException, ConfigInvalidException {
if (parent.toAccountGroup() == null) {
throw new MethodNotAllowedException();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index a8b423a..636cce6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
@@ -108,6 +109,8 @@
bind(GroupIndexCollection.class);
listener().to(GroupIndexCollection.class);
factory(GroupIndexerImpl.Factory.class);
+
+ DynamicSet.setOf(binder(), OnlineUpgradeListener.class);
}
@Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
index e40015a..8d14931 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -26,16 +27,26 @@
public class OnlineReindexer<K, V, I extends Index<K, V>> {
private static final Logger log = LoggerFactory.getLogger(OnlineReindexer.class);
+ private final String name;
private final IndexCollection<K, V, I> indexes;
private final SiteIndexer<K, V, I> batchIndexer;
- private final int version;
+ private final int oldVersion;
+ private final int newVersion;
+ private final DynamicSet<OnlineUpgradeListener> listeners;
private I index;
private final AtomicBoolean running = new AtomicBoolean();
- public OnlineReindexer(IndexDefinition<K, V, I> def, int version) {
+ public OnlineReindexer(
+ IndexDefinition<K, V, I> def,
+ int oldVersion,
+ int newVersion,
+ DynamicSet<OnlineUpgradeListener> listeners) {
+ this.name = def.getName();
this.indexes = def.getIndexCollection();
this.batchIndexer = def.getSiteIndexer();
- this.version = version;
+ this.oldVersion = oldVersion;
+ this.newVersion = newVersion;
+ this.listeners = listeners;
}
public void start() {
@@ -44,14 +55,21 @@
new Thread() {
@Override
public void run() {
+ boolean ok = false;
try {
reindex();
+ ok = true;
} finally {
running.set(false);
+ if (!ok) {
+ for (OnlineUpgradeListener listener : listeners) {
+ listener.onFailure(name, oldVersion, newVersion);
+ }
+ }
}
}
};
- t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), version));
+ t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), newVersion));
t.start();
}
}
@@ -61,7 +79,7 @@
}
public int getVersion() {
- return version;
+ return newVersion;
}
private static int version(Index<?, ?> i) {
@@ -69,9 +87,14 @@
}
private void reindex() {
+ for (OnlineUpgradeListener listener : listeners) {
+ listener.onStart(name, oldVersion, newVersion);
+ }
index =
checkNotNull(
- indexes.getWriteIndex(version), "not an active write schema version: %s", version);
+ indexes.getWriteIndex(newVersion),
+ "not an active write schema version: %s",
+ newVersion);
log.info(
"Starting online reindex from schema version {} to {}",
version(indexes.getSearchIndex()),
@@ -88,6 +111,9 @@
}
log.info("Reindex to version {} complete", version(index));
activateIndex();
+ for (OnlineUpgradeListener listener : listeners) {
+ listener.onSuccess(name, oldVersion, newVersion);
+ }
}
public void activateIndex() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java
new file mode 100644
index 0000000..a2d13fe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java
@@ -0,0 +1,45 @@
+// 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.index;
+
+/** Listener for online schema upgrade events. */
+public interface OnlineUpgradeListener {
+ /**
+ * Called before starting upgrading a single index.
+ *
+ * @param name index definition name.
+ * @param oldVersion old schema version.
+ * @param newVersion new schema version.
+ */
+ void onStart(String name, int oldVersion, int newVersion);
+
+ /**
+ * Called after successfully upgrading a single index.
+ *
+ * @param name index definition name.
+ * @param oldVersion old schema version.
+ * @param newVersion new schema version.
+ */
+ void onSuccess(String name, int oldVersion, int newVersion);
+
+ /**
+ * Called after failing to upgrade a single index.
+ *
+ * @param name index definition name.
+ * @param oldVersion old schema version.
+ * @param newVersion new schema version.
+ */
+ void onFailure(String name, int oldVersion, int newVersion);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java
new file mode 100644
index 0000000..9fc3aa9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java
@@ -0,0 +1,39 @@
+// 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.index;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+
+/** Listener to handle upgrading index schema versions at startup. */
+public class OnlineUpgrader implements LifecycleListener {
+ private final VersionManager versionManager;
+
+ @Inject
+ OnlineUpgrader(VersionManager versionManager) {
+ this.versionManager = versionManager;
+ }
+
+ @Override
+ public void start() {
+ versionManager.startOnlineUpgrade();
+ }
+
+ @Override
+ public void stop() {
+ // Do nothing; reindexing threadpools are shut down in another listener, and indexes are closed
+ // on demand by IndexCollection.
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
similarity index 77%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
index 733fcce..697c9c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
@@ -15,11 +15,13 @@
package com.google.gerrit.server.index;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexDefinition.IndexFactory;
import com.google.inject.ProvisionException;
@@ -31,7 +33,11 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
-public abstract class AbstractVersionManager implements LifecycleListener {
+public abstract class VersionManager implements LifecycleListener {
+ public static boolean getOnlineUpgrade(Config cfg) {
+ return cfg.getBoolean("index", null, "onlineUpgrade", true);
+ }
+
public static class Version<V> {
public final Schema<V> schema;
public final int version;
@@ -48,22 +54,28 @@
protected final boolean onlineUpgrade;
protected final String runReindexMsg;
protected final SitePaths sitePaths;
+
+ private final DynamicSet<OnlineUpgradeListener> listeners;
+
+ // The following fields must be accessed synchronized on this.
protected final Map<String, IndexDefinition<?, ?, ?>> defs;
protected final Map<String, OnlineReindexer<?, ?, ?>> reindexers;
- protected AbstractVersionManager(
- @GerritServerConfig Config cfg,
+ protected VersionManager(
SitePaths sitePaths,
- Collection<IndexDefinition<?, ?, ?>> defs) {
+ DynamicSet<OnlineUpgradeListener> listeners,
+ Collection<IndexDefinition<?, ?, ?>> defs,
+ boolean onlineUpgrade) {
this.sitePaths = sitePaths;
+ this.listeners = listeners;
this.defs = Maps.newHashMapWithExpectedSize(defs.size());
for (IndexDefinition<?, ?, ?> def : defs) {
this.defs.put(def.getName(), def);
}
- reindexers = Maps.newHashMapWithExpectedSize(defs.size());
- onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
- runReindexMsg =
+ this.reindexers = Maps.newHashMapWithExpectedSize(defs.size());
+ this.onlineUpgrade = onlineUpgrade;
+ this.runReindexMsg =
"No index versions for index '%s' ready; run java -jar "
+ sitePaths.gerrit_war.toAbsolutePath()
+ " reindex";
@@ -162,11 +174,37 @@
synchronized (this) {
if (!reindexers.containsKey(def.getName())) {
int latest = write.get(0).version;
- OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
+ OnlineReindexer<K, V, I> reindexer =
+ new OnlineReindexer<>(def, search.version, latest, listeners);
reindexers.put(def.getName(), reindexer);
- if (onlineUpgrade && latest != search.version) {
- reindexer.start();
- }
+ }
+ }
+ }
+
+ synchronized void startOnlineUpgrade() {
+ checkState(onlineUpgrade, "online upgrade not enabled");
+ for (IndexDefinition<?, ?, ?> def : defs.values()) {
+ String name = def.getName();
+ IndexCollection<?, ?, ?> indexes = def.getIndexCollection();
+ Index<?, ?> search = indexes.getSearchIndex();
+ checkState(
+ search != null, "no search index ready for %s; should have failed at startup", name);
+ int searchVersion = search.getSchema().getVersion();
+
+ List<Index<?, ?>> write = ImmutableList.copyOf(indexes.getWriteIndexes());
+ checkState(
+ !write.isEmpty(),
+ "no write indexes set for %s; should have been initialized at startup",
+ name);
+ int latestWriteVersion = write.get(0).getSchema().getVersion();
+
+ if (latestWriteVersion != searchVersion) {
+ OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
+ checkState(
+ reindexer != null,
+ "no reindexer found for %s; should have been initialized at startup",
+ name);
+ reindexer.start();
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
index 0587b80..0cf78be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
@@ -23,4 +23,8 @@
MigrationException(String message) {
super(message);
}
+
+ MigrationException(String message, Throwable why) {
+ super(message, why);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index a866314..62d67f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -18,6 +18,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
+import static com.google.gerrit.server.notedb.ConfigNotesMigration.SECTION_NOTE_DB;
import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB_UNFUSED;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
@@ -44,6 +45,8 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -52,11 +55,14 @@
import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.notedb.ConfigNotesMigration;
+import com.google.gerrit.server.notedb.NoteDbTable;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.notedb.NotesMigrationState;
import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
import com.google.gerrit.server.notedb.RepoSequence;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -71,6 +77,7 @@
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
import java.util.function.Predicate;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -86,12 +93,24 @@
public class NoteDbMigrator implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(NoteDbMigrator.class);
+ private static final String AUTO_MIGRATE = "autoMigrate";
+
+ public static boolean getAutoMigrate(Config cfg) {
+ return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, false);
+ }
+
+ private static void setAutoMigrate(Config cfg, boolean autoMigrate) {
+ cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, autoMigrate);
+ }
+
public static class Builder {
private final Config cfg;
private final SitePaths sitePaths;
private final SchemaFactory<ReviewDb> schemaFactory;
private final GitRepositoryManager repoManager;
private final AllProjectsName allProjects;
+ private final InternalUser.Factory userFactory;
+ private final ThreadLocalRequestContext requestContext;
private final ChangeRebuilder rebuilder;
private final WorkQueue workQueue;
private final NotesMigration globalNotesMigration;
@@ -105,6 +124,7 @@
private boolean trial;
private boolean forceRebuild;
private int sequenceGap = -1;
+ private boolean autoMigrate;
@Inject
Builder(
@@ -113,6 +133,8 @@
SchemaFactory<ReviewDb> schemaFactory,
GitRepositoryManager repoManager,
AllProjectsName allProjects,
+ ThreadLocalRequestContext requestContext,
+ InternalUser.Factory userFactory,
ChangeRebuilder rebuilder,
WorkQueue workQueue,
NotesMigration globalNotesMigration,
@@ -122,6 +144,8 @@
this.schemaFactory = schemaFactory;
this.repoManager = repoManager;
this.allProjects = allProjects;
+ this.requestContext = requestContext;
+ this.userFactory = userFactory;
this.rebuilder = rebuilder;
this.workQueue = workQueue;
this.globalNotesMigration = globalNotesMigration;
@@ -255,12 +279,29 @@
return this;
}
+ /**
+ * Enable auto-migration on subsequent daemon launches.
+ *
+ * <p>If true, prior to running any migration steps, sets the necessary configuration in {@code
+ * gerrit.config} to make {@code gerrit.war daemon} retry the migration on next startup, if it
+ * fails.
+ *
+ * @param autoMigrate whether to set auto-migration config.
+ * @return this.
+ */
+ public Builder setAutoMigrate(boolean autoMigrate) {
+ this.autoMigrate = autoMigrate;
+ return this;
+ }
+
public NoteDbMigrator build() throws MigrationException {
return new NoteDbMigrator(
sitePaths,
schemaFactory,
repoManager,
allProjects,
+ requestContext,
+ userFactory,
rebuilder,
globalNotesMigration,
primaryStorageMigrator,
@@ -273,7 +314,8 @@
stopAtState,
trial,
forceRebuild,
- sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg));
+ sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg),
+ autoMigrate);
}
}
@@ -281,6 +323,8 @@
private final SchemaFactory<ReviewDb> schemaFactory;
private final GitRepositoryManager repoManager;
private final AllProjectsName allProjects;
+ private final ThreadLocalRequestContext requestContext;
+ private final InternalUser.Factory userFactory;
private final ChangeRebuilder rebuilder;
private final NotesMigration globalNotesMigration;
private final PrimaryStorageMigrator primaryStorageMigrator;
@@ -293,12 +337,15 @@
private final boolean trial;
private final boolean forceRebuild;
private final int sequenceGap;
+ private final boolean autoMigrate;
private NoteDbMigrator(
SitePaths sitePaths,
SchemaFactory<ReviewDb> schemaFactory,
GitRepositoryManager repoManager,
AllProjectsName allProjects,
+ ThreadLocalRequestContext requestContext,
+ InternalUser.Factory userFactory,
ChangeRebuilder rebuilder,
NotesMigration globalNotesMigration,
PrimaryStorageMigrator primaryStorageMigrator,
@@ -309,7 +356,8 @@
NotesMigrationState stopAtState,
boolean trial,
boolean forceRebuild,
- int sequenceGap)
+ int sequenceGap,
+ boolean autoMigrate)
throws MigrationException {
if (!changes.isEmpty() && !projects.isEmpty()) {
throw new MigrationException("Cannot set both changes and projects");
@@ -322,6 +370,8 @@
this.rebuilder = rebuilder;
this.repoManager = repoManager;
this.allProjects = allProjects;
+ this.requestContext = requestContext;
+ this.userFactory = userFactory;
this.globalNotesMigration = globalNotesMigration;
this.primaryStorageMigrator = primaryStorageMigrator;
this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
@@ -333,6 +383,7 @@
this.trial = trial;
this.forceRebuild = forceRebuild;
this.sequenceGap = sequenceGap;
+ this.autoMigrate = autoMigrate;
}
@Override
@@ -343,7 +394,7 @@
public void migrate() throws OrmException, IOException {
if (!changes.isEmpty() || !projects.isEmpty()) {
throw new MigrationException(
- "Cannot set changes or projects during auto-migration; call rebuild() instead");
+ "Cannot set changes or projects during full migration; call rebuild() instead");
}
Optional<NotesMigrationState> maybeState = loadState();
if (!maybeState.isPresent()) {
@@ -360,6 +411,12 @@
throw new MigrationException(
"Cannot force rebuild changes; NoteDb is already the primary storage for some changes");
}
+ if (autoMigrate) {
+ if (trial) {
+ throw new MigrationException("Auto-migration cannot be used with trial mode");
+ }
+ enableAutoMigrate();
+ }
boolean rebuilt = false;
while (state.compareTo(NOTE_DB_UNFUSED) < 0) {
@@ -438,7 +495,7 @@
new RepoSequence(
repoManager,
allProjects,
- Sequences.CHANGES,
+ Sequences.NAME_CHANGES,
// If sequenceGap is 0, this writes into the sequence ref the same ID that is returned
// by the call to seq.next() below. If we actually used this as a change ID, that
// would be a problem, but we just discard it, so this is safe.
@@ -471,39 +528,40 @@
allChanges = Streams.stream(db.changes().all()).map(Change::getId).collect(toList());
}
- List<ListenableFuture<Boolean>> futures =
- allChanges
- .stream()
- .map(
- id ->
- executor.submit(
- () -> {
- // TODO(dborowitz): Avoid reopening db if using a single thread.
- try (ReviewDb db = unwrapDb(schemaFactory.open())) {
- primaryStorageMigrator.migrateToNoteDbPrimary(id);
- return true;
- } catch (Exception e) {
- log.error("Error migrating primary storage for " + id, e);
- return false;
- }
- }))
- .collect(toList());
+ try (ContextHelper contextHelper = new ContextHelper()) {
+ List<ListenableFuture<Boolean>> futures =
+ allChanges
+ .stream()
+ .map(
+ id ->
+ executor.submit(
+ () -> {
+ try (ManualRequestContext ctx = contextHelper.open()) {
+ primaryStorageMigrator.migrateToNoteDbPrimary(id);
+ return true;
+ } catch (Exception e) {
+ log.error("Error migrating primary storage for " + id, e);
+ return false;
+ }
+ }))
+ .collect(toList());
- boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
- double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
- log.info(
- String.format(
- "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
- allChanges.size(), t, allChanges.size() / t));
- if (!ok) {
- throw new MigrationException("Migrating primary storage for some changes failed, see log");
+ boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
+ double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+ log.info(
+ String.format(
+ "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
+ allChanges.size(), t, allChanges.size() / t));
+ if (!ok) {
+ throw new MigrationException("Migrating primary storage for some changes failed, see log");
+ }
}
return disableReviewDb(prev);
}
private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException {
- return saveState(prev, NOTE_DB_UNFUSED);
+ return saveState(prev, NOTE_DB_UNFUSED, c -> setAutoMigrate(c, false));
}
private Optional<NotesMigrationState> loadState() throws IOException {
@@ -518,6 +576,14 @@
private NotesMigrationState saveState(
NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException {
+ return saveState(expectedOldState, newState, c -> {});
+ }
+
+ private NotesMigrationState saveState(
+ NotesMigrationState expectedOldState,
+ NotesMigrationState newState,
+ Consumer<Config> additionalUpdates)
+ throws IOException {
synchronized (globalNotesMigration) {
// This read-modify-write is racy. We're counting on the fact that no other Gerrit operation
// modifies gerrit.config, and hoping that admins don't either.
@@ -535,6 +601,7 @@
: "But could not parse the current state"));
}
ConfigNotesMigration.setConfigValues(gerritConfig, newState.migration());
+ additionalUpdates.accept(gerritConfig);
gerritConfig.save();
// Only set in-memory state once it's been persisted to storage.
@@ -544,6 +611,16 @@
}
}
+ private void enableAutoMigrate() throws MigrationException {
+ try {
+ gerritConfig.load();
+ setAutoMigrate(gerritConfig, true);
+ gerritConfig.save();
+ } catch (ConfigInvalidException | IOException e) {
+ throw new MigrationException("Error saving auto-migration config", e);
+ }
+ }
+
public void rebuild() throws MigrationException, OrmException {
if (!globalNotesMigration.commitChangeWrites()) {
throw new MigrationException("Cannot rebuild without noteDb.changes.write=true");
@@ -552,51 +629,52 @@
log.info("Rebuilding changes in NoteDb");
List<ListenableFuture<Boolean>> futures = new ArrayList<>();
- ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
- List<Project.NameKey> projectNames =
- Ordering.usingToString().sortedCopy(changesByProject.keySet());
- for (Project.NameKey project : projectNames) {
- ListenableFuture<Boolean> future =
- executor.submit(
- () -> {
- try (ReviewDb db = unwrapDb(schemaFactory.open())) {
- return rebuildProject(db, changesByProject, project);
- } catch (Exception e) {
- log.error("Error rebuilding project " + project, e);
- return false;
- }
- });
- futures.add(future);
- }
+ try (ContextHelper contextHelper = new ContextHelper()) {
+ ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject =
+ getChangesByProject(contextHelper.getReviewDb());
+ List<Project.NameKey> projectNames =
+ Ordering.usingToString().sortedCopy(changesByProject.keySet());
+ for (Project.NameKey project : projectNames) {
+ ListenableFuture<Boolean> future =
+ executor.submit(
+ () -> {
+ try {
+ return rebuildProject(contextHelper.getReviewDb(), changesByProject, project);
+ } catch (Exception e) {
+ log.error("Error rebuilding project " + project, e);
+ return false;
+ }
+ });
+ futures.add(future);
+ }
- boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
- double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
- log.info(
- String.format(
- "Rebuilt %d changes in %.01fs (%.01f/s)\n",
- changesByProject.size(), t, changesByProject.size() / t));
- if (!ok) {
- throw new MigrationException("Rebuilding some changes failed, see log");
+ boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
+ double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+ log.info(
+ String.format(
+ "Rebuilt %d changes in %.01fs (%.01f/s)\n",
+ changesByProject.size(), t, changesByProject.size() / t));
+ if (!ok) {
+ throw new MigrationException("Rebuilding some changes failed, see log");
+ }
}
}
- private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject()
+ private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject(ReviewDb db)
throws OrmException {
// Memoize all changes so we can close the db connection and allow other threads to use the full
// connection pool.
- try (ReviewDb db = unwrapDb(schemaFactory.open())) {
- SetMultimap<Project.NameKey, Change.Id> out =
- MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
- .treeSetValues(comparing(Change.Id::get))
- .build();
- if (!projects.isEmpty()) {
- return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
- }
- if (!changes.isEmpty()) {
- return byProject(db.changes().get(changes), c -> true, out);
- }
- return byProject(db.changes().all(), c -> true, out);
+ SetMultimap<Project.NameKey, Change.Id> out =
+ MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
+ .treeSetValues(comparing(Change.Id::get))
+ .build();
+ if (!projects.isEmpty()) {
+ return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
}
+ if (!changes.isEmpty()) {
+ return byProject(db.changes().get(changes), c -> true, out);
+ }
+ return byProject(db.changes().all(), c -> true, out);
}
private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject(
@@ -658,4 +736,42 @@
return false;
}
}
+
+ private class ContextHelper implements AutoCloseable {
+ private final Thread callingThread;
+ private ReviewDb db;
+
+ ContextHelper() {
+ callingThread = Thread.currentThread();
+ }
+
+ ManualRequestContext open() throws OrmException {
+ return new ManualRequestContext(
+ userFactory.create(),
+ // Reuse the same lazily-opened ReviewDb on the original calling thread, otherwise open
+ // SchemaFactory in the normal way.
+ Thread.currentThread().equals(callingThread) ? this::getReviewDb : schemaFactory,
+ requestContext);
+ }
+
+ synchronized ReviewDb getReviewDb() throws OrmException {
+ if (db == null) {
+ db =
+ new ReviewDbWrapper(unwrapDb(schemaFactory.open())) {
+ @Override
+ public void close() {
+ // Closed by ContextHelper#close.
+ }
+ };
+ }
+ return db;
+ }
+
+ @Override
+ public synchronized void close() {
+ if (db != null) {
+ db.close();
+ }
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
new file mode 100644
index 0000000..4d5951b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
@@ -0,0 +1,89 @@
+// 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.notedb.rebuild;
+
+import com.google.common.base.Stopwatch;
+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.index.OnlineUpgrader;
+import com.google.gerrit.server.index.VersionManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class OnlineNoteDbMigrator implements LifecycleListener {
+ private static final Logger log = LoggerFactory.getLogger(OnlineNoteDbMigrator.class);
+
+ public static class Module extends LifecycleModule {
+ @Override
+ public void configure() {
+ listener().to(OnlineNoteDbMigrator.class);
+ }
+ }
+
+ private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
+ private final OnlineUpgrader indexUpgrader;
+ private final boolean upgradeIndex;
+
+ @Inject
+ OnlineNoteDbMigrator(
+ @GerritServerConfig Config cfg,
+ Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
+ OnlineUpgrader indexUpgrader) {
+ this.migratorBuilderProvider = migratorBuilderProvider;
+ this.indexUpgrader = indexUpgrader;
+ this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg);
+ }
+
+ @Override
+ public void start() {
+ Thread t = new Thread(this::migrate);
+ t.setDaemon(true);
+ t.setName(getClass().getSimpleName());
+ t.start();
+ }
+
+ private void migrate() {
+ log.info("Starting online NoteDb migration");
+ if (upgradeIndex) {
+ log.info("Online index schema upgrades will be deferred until NoteDb migration is complete");
+ }
+ Stopwatch sw = Stopwatch.createStarted();
+ // TODO(dborowitz): Tune threads, maybe expose a progress monitor somewhere.
+ try (NoteDbMigrator migrator = migratorBuilderProvider.get().setAutoMigrate(true).build()) {
+ migrator.migrate();
+ } catch (Exception e) {
+ log.error("Error in online NoteDb migration", e);
+ }
+ log.info("Online NoteDb migration completed in {}s", sw.elapsed(TimeUnit.SECONDS));
+
+ if (upgradeIndex) {
+ log.info("Starting deferred index schema upgrades");
+ indexUpgrader.start();
+ }
+ }
+
+ @Override
+ public void stop() {
+ // Do nothing; upgrade process uses daemon threads and knows how to recover from failures on
+ // next attempt.
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index e42a1cf..0c15063 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -117,8 +117,9 @@
}
}
- if (!refControl.canCreate(repo, object)) {
- throw new AuthException("Cannot create \"" + ref + "\"");
+ String rejectReason = refControl.canCreate(repo, object);
+ if (rejectReason != null) {
+ throw new AuthException("Cannot create \"" + ref + "\": " + rejectReason);
}
try {
@@ -155,7 +156,7 @@
}
refPrefix = RefUtil.getRefPrefix(refPrefix);
}
- //$FALL-THROUGH$
+ // $FALL-THROUGH$
case FORCED:
case IO_FAILURE:
case NOT_ATTEMPTED:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 2fad19b..8cd44d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.project;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -60,7 +62,7 @@
throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
}
- deleteRefFactory.create(rsrc).ref(rsrc.getRef()).delete();
+ deleteRefFactory.create(rsrc).ref(rsrc.getRef()).prefix(R_HEADS).delete();
return Response.none();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index 4b45a41..fa7e917 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.project;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
@@ -40,7 +42,7 @@
if (input == null || input.branches == null || input.branches.isEmpty()) {
throw new BadRequestException("branches must be specified");
}
- deleteRefFactory.create(project).refs(input.branches).delete();
+ deleteRefFactory.create(project).refs(input.branches).prefix(R_HEADS).delete();
return Response.none();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index de22e23..2a22b1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
@@ -259,17 +260,18 @@
*
* @param repo repository on which user want to create
* @param object the object the user will start the reference with.
- * @return {@code true} if the user specified can create a new Git ref
+ * @return {@code null} if the user specified can create a new Git ref, or a String describing why
+ * the creation is not allowed.
*/
- public boolean canCreate(Repository repo, RevObject object) {
+ @Nullable
+ public String canCreate(Repository repo, RevObject object) {
if (!isProjectStatePermittingWrite()) {
- return false;
+ return "project state does not permit write";
}
if (object instanceof RevCommit) {
if (!canPerform(Permission.CREATE)) {
- // No create permissions.
- return false;
+ return "lacks permission: " + Permission.CREATE;
}
return canCreateCommit(repo, (RevCommit) object);
} else if (object instanceof RevTag) {
@@ -277,7 +279,13 @@
try (RevWalk rw = new RevWalk(repo)) {
rw.parseBody(tag);
} catch (IOException e) {
- return false;
+ String msg =
+ String.format(
+ "RevWalk(%s) for pushing tag %s:",
+ projectControl.getProject().getNameKey(), tag.name());
+ log.error(msg, e);
+
+ return "I/O exception for revwalk";
}
// If tagger is present, require it matches the user's email.
@@ -292,18 +300,20 @@
valid = false;
}
if (!valid && !canForgeCommitter()) {
- return false;
+ return "lacks permission: " + Permission.FORGE_COMMITTER;
}
}
RevObject tagObject = tag.getObject();
if (tagObject instanceof RevCommit) {
- if (!canCreateCommit(repo, (RevCommit) tagObject)) {
- return false;
+ String rejectReason = canCreateCommit(repo, (RevCommit) tagObject);
+ if (rejectReason != null) {
+ return rejectReason;
}
} else {
- if (!canCreate(repo, tagObject)) {
- return false;
+ String rejectReason = canCreate(repo, tagObject);
+ if (rejectReason != null) {
+ return rejectReason;
}
}
@@ -311,27 +321,34 @@
// than if it doesn't have a PGP signature.
//
if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
- return canPerform(Permission.CREATE_SIGNED_TAG);
+ return canPerform(Permission.CREATE_SIGNED_TAG)
+ ? null
+ : "lacks permission: " + Permission.CREATE_SIGNED_TAG;
}
- return canPerform(Permission.CREATE_TAG);
- } else {
- return false;
+ return canPerform(Permission.CREATE_TAG) ? null : "lacks permission " + Permission.CREATE_TAG;
}
+
+ return null;
}
- private boolean canCreateCommit(Repository repo, RevCommit commit) {
+ /**
+ * Check if the user is allowed to create a new commit object if this introduces a new commit to
+ * the project. If not allowed, returns a string describing why it's not allowed.
+ */
+ @Nullable
+ private String canCreateCommit(Repository repo, RevCommit commit) {
if (canUpdate()) {
// If the user has push permissions, they can create the ref regardless
// of whether they are pushing any new objects along with the create.
- return true;
+ return null;
} else if (isMergedIntoBranchOrTag(repo, commit)) {
// If the user has no push permissions, check whether the object is
// merged into a branch or tag readable by this user. If so, they are
// not effectively "pushing" more objects, so they can create the ref
// even if they don't have push permission.
- return true;
+ return null;
}
- return false;
+ return "lacks permission " + Permission.PUSH + " for creating new commit object";
}
private boolean isMergedIntoBranchOrTag(Repository repo, RevCommit commit) {
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 6c6179b..161233e 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
@@ -486,11 +486,6 @@
@Operator
public Predicate<ChangeData> change(String query) throws QueryParseException {
- if (PAT_LEGACY_ID.matcher(query).matches()) {
- return new LegacyChangeIdPredicate(Change.Id.parse(query));
- } else if (PAT_CHANGE_ID.matcher(query).matches()) {
- return new ChangeIdPredicate(parseChangeId(query));
- }
Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
if (triplet.isPresent()) {
return Predicate.and(
@@ -498,6 +493,11 @@
branch(triplet.get().branch().get()),
new ChangeIdPredicate(parseChangeId(triplet.get().id().get())));
}
+ if (PAT_LEGACY_ID.matcher(query).matches()) {
+ return new LegacyChangeIdPredicate(Change.Id.parse(query));
+ } else if (PAT_CHANGE_ID.matcher(query).matches()) {
+ return new ChangeIdPredicate(parseChangeId(query));
+ }
throw new QueryParseException("Invalid change format");
}
@@ -732,7 +732,8 @@
}
@Operator
- public Predicate<ChangeData> label(String name) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> label(String name)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
AccountGroup.UUID group = null;
@@ -846,7 +847,8 @@
}
@Operator
- public Predicate<ChangeData> starredby(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> starredby(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return starredby(parseAccount(who));
}
@@ -863,7 +865,8 @@
}
@Operator
- public Predicate<ChangeData> watchedby(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> watchedby(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> m = parseAccount(who);
List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
@@ -887,7 +890,8 @@
}
@Operator
- public Predicate<ChangeData> draftby(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> draftby(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> m = parseAccount(who);
List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
for (Account.Id id : m) {
@@ -905,7 +909,8 @@
}
@Operator
- public Predicate<ChangeData> visibleto(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> visibleto(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
if (isSelf(who)) {
return is_visible();
}
@@ -942,12 +947,14 @@
}
@Operator
- public Predicate<ChangeData> o(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> o(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return owner(who);
}
@Operator
- public Predicate<ChangeData> owner(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> owner(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return owner(parseAccount(who));
}
@@ -960,7 +967,7 @@
}
private Predicate<ChangeData> ownerDefaultField(String who)
- throws QueryParseException, OrmException {
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = parseAccount(who);
if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
return Predicate.any();
@@ -969,7 +976,8 @@
}
@Operator
- public Predicate<ChangeData> assignee(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> assignee(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return assignee(parseAccount(who));
}
@@ -991,22 +999,24 @@
}
@Operator
- public Predicate<ChangeData> r(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> r(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return reviewer(who);
}
@Operator
- public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> reviewer(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return reviewer(who, false);
}
private Predicate<ChangeData> reviewerDefaultField(String who)
- throws QueryParseException, OrmException {
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return reviewer(who, true);
}
private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
- throws QueryParseException, OrmException {
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Predicate<ChangeData> byState =
reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
if (Objects.equals(byState, Predicate.<ChangeData>any())) {
@@ -1020,7 +1030,8 @@
}
@Operator
- public Predicate<ChangeData> cc(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> cc(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return reviewerByState(who, ReviewerStateInternal.CC, false);
}
@@ -1073,7 +1084,8 @@
}
@Operator
- public Predicate<ChangeData> commentby(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> commentby(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return commentby(parseAccount(who));
}
@@ -1086,7 +1098,8 @@
}
@Operator
- public Predicate<ChangeData> from(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> from(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> ownerIds = parseAccount(who);
return Predicate.or(owner(ownerIds), commentby(ownerIds));
}
@@ -1110,7 +1123,8 @@
}
@Operator
- public Predicate<ChangeData> reviewedby(String who) throws QueryParseException, OrmException {
+ public Predicate<ChangeData> reviewedby(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
return IsReviewedPredicate.create(parseAccount(who));
}
@@ -1192,7 +1206,7 @@
if (!Objects.equals(p, Predicate.<ChangeData>any())) {
predicates.add(p);
}
- } catch (OrmException | QueryParseException e) {
+ } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
// Skip.
}
try {
@@ -1200,13 +1214,13 @@
if (!Objects.equals(p, Predicate.<ChangeData>any())) {
predicates.add(p);
}
- } catch (OrmException | QueryParseException e) {
+ } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
// Skip.
}
predicates.add(file(query));
try {
predicates.add(label(query));
- } catch (OrmException | QueryParseException e) {
+ } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) {
// Skip.
}
predicates.add(commit(query));
@@ -1245,7 +1259,8 @@
return Predicate.and(predicates);
}
- private Set<Account.Id> parseAccount(String who) throws QueryParseException, OrmException {
+ private Set<Account.Id> parseAccount(String who)
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
if (isSelf(who)) {
return Collections.singleton(self());
}
@@ -1291,7 +1306,7 @@
public Predicate<ChangeData> reviewerByState(
String who, ReviewerStateInternal state, boolean forDefaultField)
- throws QueryParseException, OrmException {
+ throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Predicate<ChangeData> reviewerByEmailPredicate = null;
if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
Address address = Address.tryParse(who);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 9a56aa4..dfcacb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -222,11 +222,11 @@
private void initSequences(Repository git, BatchRefUpdate bru) throws IOException {
if (notesMigration.readChangeSequence()
- && git.exactRef(REFS_SEQUENCES + Sequences.CHANGES) == null) {
+ && git.exactRef(REFS_SEQUENCES + Sequences.NAME_CHANGES) == null) {
// Can't easily reuse the inserter from MetaDataUpdate, but this shouldn't slow down site
// initialization unduly.
try (ObjectInserter ins = git.newObjectInserter()) {
- bru.addCommand(RepoSequence.storeNew(ins, Sequences.CHANGES, firstChangeId));
+ bru.addCommand(RepoSequence.storeNew(ins, Sequences.NAME_CHANGES, firstChangeId));
ins.flush();
}
}
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 a0c03b6..3cfd91c 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_153> C = Schema_153.class;
+ public static final Class<Schema_154> C = Schema_154.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
index 4896f3a..6ba1b65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
@@ -20,6 +20,7 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -88,7 +89,15 @@
rewriteUserBranch(repo, rw, oi, emptyTree, ref, e.getValue());
} else {
AccountsUpdate.createUserBranch(
- repo, oi, serverIdent, serverIdent, e.getKey(), e.getValue());
+ repo,
+ allUsersName,
+ GitReferenceUpdated.DISABLED,
+ null,
+ oi,
+ serverIdent,
+ serverIdent,
+ e.getKey(),
+ e.getValue());
}
}
} catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
index 48d1e7e..29ae7d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
@@ -22,6 +22,7 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -68,7 +69,8 @@
.collect(toSet());
accountIdsFromUserBranches.removeAll(accountIdsFromReviewDb);
for (Account.Id accountId : accountIdsFromUserBranches) {
- AccountsUpdate.deleteUserBranch(repo, serverIdent, accountId);
+ AccountsUpdate.deleteUserBranch(
+ repo, allUsersName, GitReferenceUpdated.DISABLED, null, serverIdent, accountId);
}
} catch (IOException e) {
throw new OrmException("Failed to delete user branches for non-existing accounts.", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java
new file mode 100644
index 0000000..3d686bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java
@@ -0,0 +1,105 @@
+// 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 com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Migrate accounts to NoteDb. */
+public class Schema_154 extends SchemaVersion {
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final Provider<PersonIdent> serverIdent;
+
+ @Inject
+ Schema_154(
+ Provider<Schema_153> prior,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+ super(prior);
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.serverIdent = serverIdent;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+ try {
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ for (Account account : scanAccounts(db)) {
+ updateAccountInNoteDb(repo, account);
+ }
+ }
+ } catch (IOException | ConfigInvalidException e) {
+ throw new OrmException("Migrating accounts to NoteDb failed", e);
+ }
+ }
+
+ private Set<Account> scanAccounts(ReviewDb db) throws SQLException {
+ try (Statement stmt = newStatement(db);
+ ResultSet rs =
+ stmt.executeQuery(
+ "SELECT account_id,"
+ + " registered_on,"
+ + " full_name, "
+ + " preferred_email,"
+ + " status,"
+ + " inactive"
+ + " FROM accounts")) {
+ Set<Account> s = new HashSet<>();
+ while (rs.next()) {
+ Account a = new Account(new Account.Id(rs.getInt(1)), rs.getTimestamp(2));
+ a.setFullName(rs.getString(3));
+ a.setPreferredEmail(rs.getString(4));
+ a.setStatus(rs.getString(5));
+ a.setActive(rs.getString(6).equals("N"));
+ s.add(a);
+ }
+ return s;
+ }
+ }
+
+ private void updateAccountInNoteDb(Repository allUsersRepo, Account account)
+ throws IOException, ConfigInvalidException {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo);
+ PersonIdent ident = serverIdent.get();
+ md.getCommitBuilder().setAuthor(ident);
+ md.getCommitBuilder().setCommitter(ident);
+ AccountConfig accountConfig = new AccountConfig(null, account.getId());
+ accountConfig.load(allUsersRepo);
+ accountConfig.setAccount(account);
+ accountConfig.commit(md);
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index 914e074..fed0be4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -474,6 +474,14 @@
+ "\tkey1 = "
+ staff.toConfigValue()
+ "\n");
+ assertThat(text(rev, "groups"))
+ .isEqualTo(
+ "# UUID\tGroup Name\n" //
+ + "#\n" //
+ + staff.getUUID().get()
+ + " \t"
+ + staff.getName()
+ + "\n");
}
private ProjectConfig read(RevCommit rev) throws IOException, ConfigInvalidException {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 042865e..8323051 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -33,14 +33,20 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountConfig;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
@@ -58,6 +64,8 @@
import java.util.Iterator;
import java.util.List;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -82,6 +90,8 @@
@Inject protected GerritApi gApi;
+ @Inject @GerritPersonIdent Provider<PersonIdent> serverIdent;
+
@Inject protected IdentifiedUser.GenericFactory userFactory;
@Inject private Provider<AnonymousUser> anonymousUser;
@@ -98,6 +108,10 @@
@Inject protected AllProjectsName allProjects;
+ @Inject protected AllUsersName allUsers;
+
+ @Inject protected GitRepositoryManager repoManager;
+
protected LifecycleManager lifecycle;
protected Injector injector;
protected ReviewDb db;
@@ -383,12 +397,25 @@
public void reindex() throws Exception {
AccountInfo user1 = newAccountWithFullName("tester", "Test Usre");
- // update account in the database so that account index is stale
+ // update account in ReviewDb without reindex so that account index is stale
String newName = "Test User";
- Account account = accounts.get(db, new Account.Id(user1._accountId));
+ Account.Id accountId = new Account.Id(user1._accountId);
+ Account account = accounts.get(db, accountId);
account.setFullName(newName);
db.accounts().update(ImmutableSet.of(account));
+ // update account in NoteDb without reindex so that account index is stale
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsers, repo);
+ PersonIdent ident = serverIdent.get();
+ md.getCommitBuilder().setAuthor(ident);
+ md.getCommitBuilder().setCommitter(ident);
+ AccountConfig accountConfig = new AccountConfig(null, accountId);
+ accountConfig.load(repo);
+ accountConfig.getAccount().setFullName(newName);
+ accountConfig.commit(md);
+ }
+
assertQuery("name:" + quote(user1.name), user1);
assertQuery("name:" + quote(newName));
@@ -469,11 +496,16 @@
if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email));
}
- Account a = accounts.get(db, id);
- a.setFullName(fullName);
- a.setPreferredEmail(email);
- a.setActive(active);
- accountsUpdate.create().update(db, a);
+ accountsUpdate
+ .create()
+ .update(
+ db,
+ id,
+ a -> {
+ a.setFullName(fullName);
+ a.setPreferredEmail(email);
+ a.setActive(active);
+ });
return id;
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index fdb0091..2b8536b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -208,13 +208,11 @@
db = schemaFactory.open();
userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
- Account userAccount = accounts.get(db, userId);
String email = "user@example.com";
externalIdsUpdate.create().insert(ExternalId.createEmail(userId, email));
- userAccount.setPreferredEmail(email);
- accountsUpdate.create().update(db, userAccount);
+ accountsUpdate.create().update(db, userId, a -> a.setPreferredEmail(email));
user = userFactory.create(userId);
- requestContext.setContext(newRequestContext(userAccount.getId()));
+ requestContext.setContext(newRequestContext(userId));
}
protected RequestContext newRequestContext(Account.Id requestUserId) {
@@ -289,25 +287,25 @@
@Test
public void byTriplet() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ TestRepository<Repo> repo = createProject("iabcde");
Change change = insert(repo, newChangeForBranch(repo, "branch"));
String k = change.getKey().get();
- assertQuery("repo~branch~" + k, change);
- assertQuery("change:repo~branch~" + k, change);
- assertQuery("repo~refs/heads/branch~" + k, change);
- assertQuery("change:repo~refs/heads/branch~" + k, change);
- assertQuery("repo~branch~" + k.substring(0, 10), change);
- assertQuery("change:repo~branch~" + k.substring(0, 10), change);
+ assertQuery("iabcde~branch~" + k, change);
+ assertQuery("change:iabcde~branch~" + k, change);
+ assertQuery("iabcde~refs/heads/branch~" + k, change);
+ assertQuery("change:iabcde~refs/heads/branch~" + k, change);
+ assertQuery("iabcde~branch~" + k.substring(0, 10), change);
+ assertQuery("change:iabcde~branch~" + k.substring(0, 10), change);
assertQuery("foo~bar");
assertThatQueryException("change:foo~bar").hasMessageThat().isEqualTo("Invalid change format");
assertQuery("otherrepo~branch~" + k);
assertQuery("change:otherrepo~branch~" + k);
- assertQuery("repo~otherbranch~" + k);
- assertQuery("change:repo~otherbranch~" + k);
- assertQuery("repo~branch~I0000000000000000000000000000000000000000");
- assertQuery("change:repo~branch~I0000000000000000000000000000000000000000");
+ assertQuery("iabcde~otherbranch~" + k);
+ assertQuery("change:iabcde~otherbranch~" + k);
+ assertQuery("iabcde~branch~I0000000000000000000000000000000000000000");
+ assertQuery("change:iabcde~branch~I0000000000000000000000000000000000000000");
}
@Test
@@ -2340,11 +2338,16 @@
if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email));
}
- Account a = accounts.get(db, id);
- a.setFullName(fullName);
- a.setPreferredEmail(email);
- a.setActive(active);
- accountsUpdate.create().update(db, a);
+ accountsUpdate
+ .create()
+ .update(
+ db,
+ id,
+ a -> {
+ a.setFullName(fullName);
+ a.setPreferredEmail(email);
+ a.setActive(active);
+ });
return id;
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index fe2a8f3..0de5fad 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -319,11 +319,16 @@
if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email));
}
- Account a = accounts.get(db, id);
- a.setFullName(fullName);
- a.setPreferredEmail(email);
- a.setActive(active);
- accountsUpdate.create().update(db, a);
+ accountsUpdate
+ .create()
+ .update(
+ db,
+ id,
+ a -> {
+ a.setFullName(fullName);
+ a.setPreferredEmail(email);
+ a.setActive(active);
+ });
return id;
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 4bb176c..a742c35 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -37,6 +37,7 @@
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -103,7 +104,7 @@
@Inject private AddIncludedGroups addIncludedGroups;
@Override
- protected void run() throws Failure, OrmException, IOException {
+ protected void run() throws Failure, OrmException, IOException, ConfigInvalidException {
try {
GroupResource rsrc = createGroup();
@@ -119,7 +120,8 @@
}
}
- private GroupResource createGroup() throws RestApiException, OrmException, IOException {
+ private GroupResource createGroup()
+ throws RestApiException, OrmException, IOException, ConfigInvalidException {
GroupInput input = new GroupInput();
input.description = groupDescription;
input.visibleToAll = visibleToAll;
@@ -132,7 +134,8 @@
return groups.parse(TopLevelResource.INSTANCE, IdString.fromUrl(group.id));
}
- private void addMembers(GroupResource rsrc) throws RestApiException, OrmException, IOException {
+ private void addMembers(GroupResource rsrc)
+ throws RestApiException, OrmException, IOException, ConfigInvalidException {
AddMembers.Input input =
AddMembers.Input.fromMembers(
initialMembers.stream().map(Object::toString).collect(toList()));
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 4d93fe8..bbe736d 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
@@ -35,6 +35,7 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -79,7 +80,7 @@
Account userAccount;
try {
userAccount = accountResolver.find(db, userName);
- } catch (OrmException e) {
+ } catch (OrmException | IOException | ConfigInvalidException e) {
throw die(e);
}
if (userAccount == null) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 1923169..033b4c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -292,7 +292,8 @@
}
private void putPreferred(String email)
- throws RestApiException, OrmException, IOException, PermissionBackendException {
+ throws RestApiException, OrmException, IOException, PermissionBackendException,
+ ConfigInvalidException {
for (EmailInfo e : getEmails.apply(rsrc)) {
if (e.email.equals(email)) {
putPreferred.apply(new AccountResource.Email(user, email), null);
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
index 9b473c0..bf10043 100644
--- a/lib/codemirror/cm.bzl
+++ b/lib/codemirror/cm.bzl
@@ -214,7 +214,7 @@
"z80",
]
-CM_VERSION = "5.26.0"
+CM_VERSION = "5.27.2"
TOP = "META-INF/resources/webjars/codemirror/%s" % CM_VERSION
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index bb047bd70..9c3763e 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -174,6 +174,15 @@
seed = True,
)
bower_component(
+ name = "polymer-resin",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":polymer",
+ ":webcomponentsjs",
+ ],
+ seed = True,
+ )
+ bower_component(
name = "promise-polyfill",
license = "//lib:LICENSE-promise-polyfill",
deps = [ ":polymer" ],
diff --git a/plugins/replication b/plugins/replication
index 6d83f5e..fae5360 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 6d83f5ebfae04717d13cfadda4ca526fd66a5918
+Subproject commit fae5360380023e8351f39be3d4effd4bb2cd8906
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index fd75f5f..d4d2322 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -21,6 +21,7 @@
"//lib/js:moment",
"//lib/js:page",
"//lib/js:polymer",
+ "//lib/js:polymer-resin",
"//lib/js:promise-polyfill",
],
)
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index f334354..04a1d69 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -2,6 +2,7 @@
default_visibility = ["//visibility:public"],
)
+load(":rules.bzl", "polygerrit_bundle")
load("//tools/bzl:genrule2.bzl", "genrule2")
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary")
load(
@@ -12,8 +13,8 @@
"js_component",
)
-vulcanize(
- name = "gr-app",
+polygerrit_bundle(
+ name = "polygerrit_ui",
srcs = glob(
[
"**/*.html",
@@ -27,82 +28,7 @@
],
),
app = "elements/gr-app.html",
- deps = ["//polygerrit-ui:polygerrit_components.bower_components"],
-)
-
-closure_js_library(
- name = "closure_lib",
- srcs = ["gr-app.js"],
- convention = "GOOGLE",
- # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
- # and remove this supression
- suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"],
- deps = [
- "//lib/polymer_externs:polymer_closure",
- "@io_bazel_rules_closure//closure/library",
- ],
-)
-
-closure_js_binary(
- name = "closure_bin",
- # Known issue: Closure compilation not compatible with Polymer behaviors.
- # See: https://github.com/google/closure-compiler/issues/2042
- compilation_level = "WHITESPACE_ONLY",
- defs = [
- "--polymer_pass",
- "--jscomp_off=duplicate",
- "--force_inject_library=es6_runtime",
- ],
- language = "ECMASCRIPT5",
- deps = [":closure_lib"],
-)
-
-filegroup(
- name = "top_sources",
- srcs = [
- "favicon.ico",
- "index.html",
- ],
-)
-
-filegroup(
- name = "css_sources",
- srcs = glob(["styles/**/*.css"]),
-)
-
-filegroup(
- name = "app_sources",
- srcs = [
- "closure_bin.js",
- "gr-app.html",
- ],
-)
-
-genrule2(
- name = "polygerrit_ui",
- srcs = [
- "//lib/fonts:robotomono",
- "//lib/js:highlightjs_files",
- ":top_sources",
- ":css_sources",
- ":app_sources",
- # we extract from the zip, but depend on the component for license checking.
- "@webcomponentsjs//:zipfile",
- "//lib/js:webcomponentsjs",
- ],
outs = ["polygerrit_ui.zip"],
- cmd = " && ".join([
- "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
- "for f in $(locations :app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/gr-app.$$ext; done",
- "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/",
- "for f in $(locations :top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
- "for f in $(locations :css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
- "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
- "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js",
- "cd $$TMP",
- "find . -exec touch -t 198001010000 '{}' ';'",
- "zip -qr $$ROOT/$@ *",
- ]),
)
bower_component_bundle(
@@ -198,3 +124,49 @@
"manual",
],
)
+
+# Embed bundle
+polygerrit_bundle(
+ name = "polygerrit_embed_ui",
+ srcs = glob(
+ [
+ "**/*.html",
+ "**/*.js",
+ ],
+ exclude = [
+ "bower_components/**",
+ "index.html",
+ "test/**",
+ "**/*_test.html",
+ ],
+ ),
+ app = "embed/change-diff-views.html",
+ outs = ["polygerrit_embed_ui.zip"],
+)
+
+filegroup(
+ name = "embed_test_files",
+ srcs = glob(
+ [
+ "embed/**/*_test.html",
+ ],
+ ),
+)
+
+sh_test(
+ name = "embed_test",
+ size = "large",
+ srcs = ["embed_test.sh"],
+ data = [
+ "test/common-test-setup.html",
+ "embed/test.html",
+ ":embed_test_files",
+ ":polygerrit_embed_ui.zip",
+ ":test_components.zip",
+ ],
+ # Should not run sandboxed.
+ tags = [
+ "local",
+ "manual",
+ ],
+)
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index ab9158d..22a2abc 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -31,7 +31,7 @@
items-per-page="[[_groupsPerPage]]"
loading="[[_loading]]"
offset="[[_offset]]"
- path="/admin/groups">
+ path="[[_path]]">
<table id="list">
<tr class="headerRow">
<th class="name topHeader">Group Name</th>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 130e4f4..645b89b 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -33,7 +33,7 @@
_path: {
type: String,
readOnly: true,
- value: '/admin/groups/',
+ value: '/admin/groups',
},
_groups: Array,
@@ -77,7 +77,7 @@
},
_computeGroupUrl(id) {
- return this.getUrl(this._path, id);
+ return this.getUrl(this._path + '/', id);
},
_getGroups(filter, groupsPerPage, offset) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
index 05b30e2..45a6e4a 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -32,7 +32,7 @@
items="[[_projects]]"
loading="[[_loading]]"
offset="[[_offset]]"
- path="/admin/projects">
+ path="[[_path]]">
<table id="list">
<tr class="headerRow">
<th class="name topHeader">Project Name</th>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
index 41a4737..8cd804f 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -33,7 +33,7 @@
_path: {
type: String,
readOnly: true,
- value: '/admin/projects/',
+ value: '/admin/projects',
},
_projects: Array,
@@ -72,7 +72,7 @@
},
_computeProjectUrl(name) {
- return this.getUrl(this._path, name);
+ return this.getUrl(this._path + '/', name);
},
_getProjects(filter, projectsPerPage, offset) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
index 8829506..1292bc5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
@@ -33,7 +33,7 @@
</test-fixture>
<script>
- let counter = 0;
+ let counter;
const projectGenerator = () => {
return {
id: `test${++counter}`,
@@ -56,6 +56,7 @@
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
+ counter = 0;
});
teardown(() => {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
index a9dd3df8..e149594 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
@@ -97,7 +97,7 @@
id="submitTypeSelect"
is="gr-select"
bind-value="{{_projectConfig.submit_type}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat" items="[[_submitTypes]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
@@ -111,7 +111,7 @@
id="contentMergeSelect"
is="gr-select"
bind-value="{{_projectConfig.use_content_merge.configured_value}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_projectConfig.use_content_merge)]]">
<option value="[[item.value]]">[[item.label]]</option>
@@ -128,7 +128,7 @@
id="newChangeSelect"
is="gr-select"
bind-value="{{_projectConfig.create_new_change_for_all_not_in_target.configured_value}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_projectConfig.create_new_change_for_all_not_in_target)]]">
<option value="[[item.value]]">[[item.label]]</option>
@@ -143,7 +143,7 @@
id="requireChangeIdSelect"
is="gr-select"
bind-value="{{_projectConfig.require_change_id.configured_value}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_projectConfig.require_change_id)]]">
<option value="[[item.value]]">[[item.label]]</option>
@@ -159,7 +159,7 @@
id="rejectImplicitMergesSelect"
is="gr-select"
bind-value="{{_projectConfig.reject_implicit_merges.configured_value}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_projectConfig.reject_implicit_merges)]]">
<option value="[[item.value]]">[[item.label]]</option>
@@ -178,6 +178,21 @@
disabled$="[[_readOnly]]">
</span>
</section>
+ <section>
+ <span class="title">Match authored date with committer date upon submit</span>
+ <span class="value">
+ <select
+ id="matchAuthoredDateWithCommitterDateSelect"
+ is="gr-select"
+ bind-value="{{_projectConfig.match_author_to_committer_date.configured_value}}"
+ disabled$="[[_readOnly]]">
+ <template is="dom-repeat"
+ items="[[_formatBooleanSelect(_projectConfig.match_author_to_committer_date)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </span>
+ </section>
</fieldset>
<h3 id="Options">Contributor Agreements</h3>
<fieldset id="agreements">
@@ -189,7 +204,7 @@
id="contributorAgreementSelect"
is="gr-select"
bind-value="{{_projectConfig.use_contributor_agreements.configured_value}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_projectConfig.use_contributor_agreements)]]">
<option value="[[item.value]]">[[item.label]]</option>
@@ -204,7 +219,7 @@
id="useSignedOffBySelect"
is="gr-select"
bind-value="{{_projectConfig.use_signed_off_by.configured_value}}"
- disabled$="[[_readOnly]]">>
+ disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_projectConfig.use_signed_off_by)]]">
<option value="[[item.value]]">[[item.label]]</option>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
index 031720a..e3b809d 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
@@ -77,6 +77,10 @@
value: false,
configured_value: 'FALSE',
},
+ match_author_to_committer_date: {
+ value: false,
+ configured_value: 'FALSE',
+ },
max_object_size_limit: {},
submit_type: 'MERGE_IF_NECESSARY',
});
@@ -225,6 +229,7 @@
create_new_change_for_all_not_in_target: 'TRUE',
require_change_id: 'TRUE',
reject_implicit_merges: 'TRUE',
+ match_author_to_committer_date: 'TRUE',
max_object_size_limit: 10,
submit_type: 'FAST_FORWARD_ONLY',
state: 'READ_ONLY',
@@ -251,6 +256,8 @@
configInputObj.require_change_id;
element.$.rejectImplicitMergesSelect.bindValue =
configInputObj.reject_implicit_merges;
+ element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
+ configInputObj.match_author_to_committer_date;
element.$.maxGitObjSizeInput.bindValue =
configInputObj.max_object_size_limit;
element.$.contributorAgreementSelect.bindValue =
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index 80db48b..91946a6 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -17,6 +17,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
@@ -27,6 +28,7 @@
<link rel="import" href="../gr-admin-plugin-list/gr-admin-plugin-list.html">
<link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html">
<link rel="import" href="../gr-admin-project/gr-admin-project.html">
+<link rel="import" href="../gr-project-branches/gr-project-branches.html">
<dom-module id="gr-admin-view">
<template>
@@ -34,17 +36,32 @@
<style include="gr-menu-page-styles"></style>
<gr-page-nav class$="[[_computeLoadingClass(_loading)]]">
<ul class="sectionContent">
- <template is="dom-repeat" items="[[_filteredLinks]]">
+ <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
<li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
<a class="title" href="[[_computeLinkURL(item)]]"
- rel$="[[_computeLinkRel(item)]]">[[item.name]]</a>
+ rel="noopener">[[item.name]]</a>
</li>
- <template is="dom-repeat" items="[[item.children]]">
- <li class$="[[_computeSelectedClass(item.view, params)]]">
- <a href="[[_computeLinkURL(item)]]"
- rel$="[[_computeLinkRel(item)]]">[[item.name]]</a>
+ <template is="dom-repeat" items="[[item.children]]" as="child">
+ <li class$="[[_computeSelectedClass(child.view, params)]]">
+ <a href$="[[_computeLinkURL(child)]]"
+ rel="noopener">[[child.name]]</a>
</li>
</template>
+ <template is="dom-if" if="[[item.subsection]]">
+ <!--If a section has a subsection, render that.-->
+ <li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
+ <a class="title" href$="[[_computeLinkURL(item.subsection)]]"
+ rel="noopener">
+ [[item.subsection.name]]</a>
+ </li>
+ <!--Loop through the links in the sub-section.-->
+ <template is="dom-repeat"
+ items="[[item.subsection.children]]" as="child">
+ <li class$="subsectionItem [[_computeSelectedClass(child.view, params)]]">
+ <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
+ </li>
+ </template>
+ </template>
</template>
</ul>
</gr-page-nav>
@@ -77,6 +94,13 @@
id="createProject"></gr-admin-create-project>
</main>
</template>
+ <template is="dom-if" if="[[_showProjectBranches]]" restamp="true">
+ <main class="table">
+ <gr-project-branches
+ params="[[params]]"
+ class="table"></gr-project-branches>
+ </main>
+ </template>
<template is="dom-if" if="[[params.placeholder]]" restamp="true">
<gr-placeholder title="Admin" path="[[path]]"></gr-placeholder>
</template>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 66105a1..e69d5be 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -55,6 +55,7 @@
path: String,
adminView: String,
+ _project: String,
_filteredLinks: Array,
_showDownload: {
type: Boolean,
@@ -63,12 +64,14 @@
_showCreateProject: Boolean,
_showProjectMain: Boolean,
_showProjectList: Boolean,
+ _showProjectBranches: Boolean,
_showGroupList: Boolean,
_showPluginList: Boolean,
},
behaviors: [
Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
],
observers: [
@@ -94,10 +97,29 @@
_filterLinks(filterFn) {
const links = ADMIN_LINKS.filter(filterFn);
+ const filteredLinks = [];
for (const link of links) {
- link.children = link.children ? link.children.filter(filterFn) : [];
+ const linkCopy = Object.assign({}, link);
+ linkCopy.children = linkCopy.children ?
+ linkCopy.children.filter(filterFn) : [];
+ if (linkCopy.name === 'Projects' && this._project) {
+ linkCopy.subsection = {
+ name: `${this._project}`,
+ view: 'gr-admin-project',
+ url: `/admin/projects/${this.encodeURL(this._project, true)}`,
+ children: [
+ {
+ name: 'Branches',
+ view: 'gr-project-branches',
+ url: `/admin/projects/${this.encodeURL(this._project, true)}` +
+ ',branches',
+ },
+ ],
+ };
+ }
+ filteredLinks.push(linkCopy);
}
- return links;
+ return filteredLinks;
},
_loadAccountCapabilities() {
@@ -110,37 +132,21 @@
});
},
- _computeSideLinks(unformattedLinks) {
- const topLevelLinks = unformattedLinks.filter(link => {
- return link.topLevel;
- });
-
- const nestedLinks = unformattedLinks.filter(link => {
- return !link.topLevel;
- });
-
- return topLevelLinks.map(item => {
- const section = {
- name: item.name,
- url: item.url,
- view: item.view,
- };
- const newLinks = nestedLinks.filter(group => {
- return group.section === section.name;
- });
- section.links = newLinks;
- return section;
- });
- },
-
_paramsChanged(params) {
this.set('_showCreateProject',
params.adminView === 'gr-admin-create-project');
this.set('_showProjectMain', params.adminView === 'gr-admin-project');
this.set('_showProjectList',
params.adminView === 'gr-admin-project-list');
+ this.set('_showProjectBranches',
+ params.adminView === 'gr-project-branches');
this.set('_showGroupList', params.adminView === 'gr-admin-group-list');
this.set('_showPluginList', params.adminView === 'gr-admin-plugin-list');
+ if (params.project !== this._project) {
+ this._project = params.project || '';
+ // Reloads the admin menu.
+ this.reload();
+ }
},
// TODO (beckysiegel): Update these functions after router abstraction is
@@ -156,19 +162,13 @@
},
_computeLinkURL(link) {
- if (typeof link.url === 'undefined') {
- return '';
- }
+ if (!link || typeof link.url === 'undefined') { return ''; }
if (link.target) {
return link.url;
}
return this._computeRelativeURL(link.url);
},
- _computeLinkRel(link) {
- return link.target ? 'noopener' : null;
- },
-
_computeSelectedClass(itemView, params) {
return itemView === params.adminView ? 'selected' : '';
},
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 721bcc0..acb3ba4 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -39,6 +39,11 @@
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getProjectConfig() {
+ return Promise.resolve({});
+ },
+ });
});
teardown(() => {
@@ -99,6 +104,7 @@
// Projects
assert.equal(element._filteredLinks[0].children.length, 1);
+ assert.isNotOk(element._filteredLinks[0].subsection);
// Groups
assert.equal(element._filteredLinks[1].children.length, 1);
@@ -118,6 +124,7 @@
// Projects
assert.equal(element._filteredLinks[0].children.length, 0);
+ assert.isNotOk(element._filteredLinks[0].subsection);
// Groups
assert.equal(element._filteredLinks[1].children.length, 0);
@@ -131,8 +138,53 @@
// Projects
assert.equal(element._filteredLinks[0].children.length, 0);
+ assert.isNotOk(element._filteredLinks[0].subsection);
done();
});
});
+
+ test('Project shows up in nav', done => {
+ element._project = 'Test Project';
+ sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
+ return Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ });
+ });
+ element._loadAccountCapabilities().then(() => {
+ assert.equal(element._filteredLinks.length, 3);
+
+ // Projects
+ assert.equal(element._filteredLinks[0].children.length, 1);
+ assert.equal(element._filteredLinks[0].subsection.name, 'Test Project');
+
+ // Groups
+ assert.equal(element._filteredLinks[1].children.length, 1);
+
+ // Plugins
+ assert.equal(element._filteredLinks[2].children.length, 0);
+ done();
+ });
+ });
+
+ test('Nav is reloaded when project changes', () => {
+ sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
+ return Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ });
+ });
+ sandbox.stub(element.$.restAPI, 'getAccount', () => {
+ return Promise.resolve({_id: 1});
+ });
+ sandbox.stub(element, 'reload');
+ element.params = {project: 'Test Project', adminView: 'gr-admin-project'};
+ assert.equal(element.reload.callCount, 1);
+ element.params = {project: 'Test Project 2',
+ adminView: 'gr-admin-project'};
+ assert.equal(element.reload.callCount, 2);
+ });
});
</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html
new file mode 100644
index 0000000..063b311
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.html
@@ -0,0 +1,69 @@
+<!--
+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="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-list-view/gr-list-view.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-project-branches">
+ <template>
+ <style include="shared-styles"></style>
+ <gr-list-view
+ filter="[[_filter]]"
+ items-per-page="[[_branchesPerPage]]"
+ items="[[_branches]]"
+ loading="[[_loading]]"
+ offset="[[_offset]]"
+ path="[[_getPath(_project)]]">
+ <table id="list">
+ <tr class="headerRow">
+ <th class="name topHeader">Branch Name</th>
+ <th class="description topHeader">Revision</th>
+ <th class="repositoryBrowser topHeader">Repository Browser</th>
+ </tr>
+ <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ <template is="dom-repeat" items="[[_shownBranches]]"
+ class$="[[computeLoadingClass(_loading)]]">
+ <tr class="table">
+ <td class="name">[[_stripRefsHeads(item.ref)]]</td>
+ <td class="description">[[item.revision]]</td>
+ <td class="repositoryBrowser">
+ <template is="dom-repeat"
+ items="[[_computeWeblink(item)]]" as="link">
+ <a href$="[[link.url]]"
+ class="webLink"
+ rel="noopener"
+ target="_blank">
+ ([[link.name]])
+ </a>
+ </template>
+ </td>
+ </tr>
+ </template>
+ </table>
+ </gr-list-view>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-project-branches.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js
new file mode 100644
index 0000000..97e3c4d
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches.js
@@ -0,0 +1,96 @@
+// 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-project-branches',
+
+ properties: {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _project: Object,
+ _branches: Array,
+ /**
+ * Because we request one more than the projectsPerPage, _shownProjects
+ * maybe one less than _projects.
+ * */
+ _shownBranches: {
+ type: Array,
+ computed: 'computeShownItems(_branches)',
+ },
+ _branchesPerPage: {
+ type: Number,
+ value: 25,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: String,
+ },
+
+ behaviors: [
+ Gerrit.ListViewBehavior,
+ Gerrit.URLEncodingBehavior,
+ ],
+
+ _paramsChanged(params) {
+ this._loading = true;
+ if (!params || !params.project) { return; }
+
+ this._project = params.project;
+
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getBranches(this._filter, this._project,
+ this._branchesPerPage, this._offset);
+ },
+
+ _getBranches(filter, project, projectsPerPage, offset) {
+ this._projectsBranches = [];
+ return this.$.restAPI.getProjectBranches(
+ filter, project, projectsPerPage, offset) .then(branches => {
+ if (!branches) { return; }
+ this._branches = branches;
+ this._loading = false;
+ });
+ },
+
+ _getPath(project) {
+ return '/admin/projects/' + this.encodeURL(project, true) + ',branches';
+ },
+
+ _computeWeblink(project) {
+ if (!project.web_links) { return ''; }
+ const webLinks = project.web_links;
+ return webLinks.length ? webLinks : null;
+ },
+
+ _stripRefsHeads(item) {
+ return item.replace('refs/heads/', '');
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html
new file mode 100644
index 0000000..707a536
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-project-branches/gr-project-branches_test.html
@@ -0,0 +1,150 @@
+<!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-project-branches</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<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-project-branches.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-project-branches></gr-project-branches>
+ </template>
+</test-fixture>
+
+<script>
+ let counter;
+ const branchGenerator = () => {
+ return {
+ ref: `refs/heads/test${++counter}`,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
+ },
+ ],
+ };
+ };
+
+ suite('gr-project-branches tests', () => {
+ let element;
+ let branches;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with project branches', () => {
+ setup(done => {
+ branches = _.times(26, branchGenerator);
+
+ stub('gr-rest-api-interface', {
+ getProjectBranches(num, project, offset) {
+ return Promise.resolve(branches);
+ },
+ });
+
+ const params = {
+ project: 'test',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('test for test branch in the list', done => {
+ flush(() => {
+ assert.equal(element._branches[1].ref, 'refs/heads/test2');
+ done();
+ });
+ });
+
+ test('test for test web links in the branches list', done => {
+ flush(() => {
+ assert.equal(element._branches[1].web_links[0].url,
+ 'https://git.example.org/branch/test;refs/heads/test2');
+ done();
+ });
+ });
+
+ test('test for refs/heads/ being striped from ref', done => {
+ flush(() => {
+ assert.equal(element._stripRefsHeads(element._branches[1].ref),
+ 'test2');
+ done();
+ });
+ });
+
+ test('_shownBranches', () => {
+ assert.equal(element._shownBranches.length, 25);
+ });
+ });
+
+ suite('list with less then 25 branches', () => {
+ setup(done => {
+ branches = _.times(25, branchGenerator);
+
+ stub('gr-rest-api-interface', {
+ getProjectBranches(num, project, offset) {
+ return Promise.resolve(branches);
+ },
+ });
+
+ const params = {
+ project: 'test',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('_shownProjectsBranches', () => {
+ assert.equal(element._shownBranches.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(element.$.restAPI, 'getProjectBranches', () => {
+ return Promise.resolve(branches);
+ });
+ const params = {
+ project: 'test',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params).then(() => {
+ assert.isTrue(element.$.restAPI.getProjectBranches.lastCall
+ .calledWithExactly('test', 'test', 25, 25));
+ done();
+ });
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
index 1288094..1dcae68 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -45,7 +45,7 @@
data-account-id$="[[account._account_id]]"
removable="[[_computeRemovable(account)]]"
on-keydown="_handleChipKeydown"
- tabindex$="[[index]]">
+ tabindex="-1">
</gr-account-chip>
</template>
<gr-account-entry
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 7a61d56..5b5ece6 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
@@ -252,6 +252,9 @@
flex-direction: column;
flex-wrap: nowrap;
}
+ #commitMessageEditor {
+ min-width: 0;
+ }
}
/* NOTE: If you update this breakpoint, also update the
BREAKPOINT_RELATED_SMALL in the JS */
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 d3ae4b2..f8a1ea2 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
@@ -73,6 +73,7 @@
type: Object,
notify: true,
value() { return {}; },
+ observer: '_viewStateChanged',
},
backPage: String,
hasParent: Boolean,
@@ -87,6 +88,7 @@
_diffPrefs: Object,
_numFilesShown: {
type: Number,
+ value: DEFAULT_NUM_FILES_SHOWN,
observer: '_numFilesShownChanged',
},
_account: {
@@ -221,9 +223,6 @@
}
});
- this._numFilesShown = this.viewState.numFilesShown ?
- this.viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
-
this.addEventListener('comment-save', this._handleCommentSave.bind(this));
this.addEventListener('comment-discard',
this._handleCommentDiscard.bind(this));
@@ -512,6 +511,11 @@
}
},
+ _viewStateChanged(viewState) {
+ this._numFilesShown = viewState.numFilesShown ?
+ viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
+ },
+
_numFilesShownChanged(numFilesShown) {
this.viewState.numFilesShown = numFilesShown;
},
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index c1640fa..010a01f 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -93,8 +93,14 @@
this._reviewers = result.filter(reviewer => {
return reviewer._account_id != owner._account_id;
});
- this._displayedReviewers =
+ // If there is one more than the max reviewers, don't show the 'show more'
+ // button, because it takes up just as much space.
+ if (this._reviewers.length <= MAX_REVIEWERS_DISPLAYED + 1) {
+ this._displayedReviewers = this._reviewers;
+ } else {
+ this._displayedReviewers =
this._reviewers.slice(0, MAX_REVIEWERS_DISPLAYED);
+ }
},
_computeHiddenCount(reviewers, displayedReviewers) {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 4e0541e6..3c1773d 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -188,9 +188,9 @@
{value: {ccsOnly: true}}));
});
- test('no show all reviewers button with 5 reviewers', () => {
+ test('no show all reviewers button with 6 reviewers', () => {
const reviewers = [];
- for (let i = 0; i < 5; i++) {
+ for (let i = 0; i < 6; i++) {
reviewers.push(
{email: i+'reviewer@google.com', name: 'reviewer-' + i});
}
@@ -206,11 +206,34 @@
};
flushAsynchronousOperations();
assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 5);
- assert.equal(element._reviewers.length, 5);
+ assert.equal(element._displayedReviewers.length, 6);
+ assert.equal(element._reviewers.length, 6);
assert.isTrue(element.$$('.hiddenReviewers').hidden);
});
+ test('how all reviewers button with 7 reviewers', () => {
+ const reviewers = [];
+ for (let i = 0; i < 7; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ flushAsynchronousOperations();
+ assert.equal(element._hiddenReviewerCount, 2);
+ assert.equal(element._displayedReviewers.length, 5);
+ assert.equal(element._reviewers.length, 7);
+ assert.isFalse(element.$$('.hiddenReviewers').hidden);
+ });
+
test('show all reviewers button', () => {
const reviewers = [];
for (let i = 0; i < 100; i++) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index b8d1f47..1b89783 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -174,6 +174,38 @@
});
});
+ // Matches /admin/projects/<project/branches[,<offset>].
+ page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => {
+ app.params = {
+ view: 'gr-admin-view',
+ adminView: 'gr-project-branches',
+ project: data.params[0],
+ offset: data.params[2] || 0,
+ filter: null,
+ };
+ });
+
+ page('/admin/projects/:project,branches/q/filter::filter,:offset',
+ loadUser, data => {
+ app.params = {
+ view: 'gr-admin-view',
+ adminView: 'gr-project-branches',
+ project: data.params.project,
+ offset: data.params.offset,
+ filter: data.params.filter,
+ };
+ });
+
+ page('/admin/projects/:project,branches/q/filter::filter',
+ loadUser, data => {
+ app.params = {
+ view: 'gr-admin-view',
+ adminView: 'gr-project-branches',
+ project: data.params.project,
+ filter: data.params.filter || null,
+ };
+ });
+
// Matches /admin/projects[,<offset>][/].
page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => {
app.params = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 895f777..330fefe 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -94,6 +94,47 @@
}
},
+ /**
+ * Get current normalized selection.
+ * Merges multiple ranges, accounts for triple click, accounts for
+ * syntax highligh, convert native DOM Range objects to Gerrit concepts
+ * (line, side, etc).
+ * @return {{
+ * start: {
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * },
+ * end: {
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * }
+ * }}
+ */
+ _getNormalizedRange() {
+ const selection = window.getSelection();
+ const rangeCount = selection.rangeCount;
+ if (rangeCount === 0) {
+ return null;
+ } else if (rangeCount === 1) {
+ return this._normalizeRange(selection.getRangeAt(0));
+ } else {
+ const startRange = this._normalizeRange(selection.getRangeAt(0));
+ const endRange = this._normalizeRange(
+ selection.getRangeAt(rangeCount - 1));
+ return {
+ start: startRange.start,
+ end: endRange.end,
+ };
+ }
+ },
+
+ /**
+ * Normalize a specific DOM Range.
+ */
_normalizeRange(domRange) {
const range = GrRangeNormalizer.normalize(domRange);
return this._fixTripleClickSelection({
@@ -204,15 +245,11 @@
},
_handleSelection() {
- const selection = window.getSelection();
- if (selection.rangeCount != 1) {
+ const normalizedRange = this._getNormalizedRange();
+ if (!normalizedRange) {
return;
}
- const range = selection.getRangeAt(0);
- if (range.collapsed) {
- return;
- }
- const normalizedRange = this._normalizeRange(range);
+ const domRange = window.getSelection().getRangeAt(0);
const start = normalizedRange.start;
if (!start) {
return;
@@ -239,7 +276,7 @@
};
actionBox.side = start.side;
if (start.line === end.line) {
- actionBox.placeAbove(range);
+ actionBox.placeAbove(domRange);
} else if (start.node instanceof Text) {
actionBox.placeAbove(start.node.splitText(start.column));
start.node.parentElement.normalize(); // Undo splitText from above.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index f8e2a77..b63b9a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -298,6 +298,37 @@
assert.equal(getActionSide(), 'right');
});
+ test('multiple ranges aka firefox implementation', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+
+ const startRange = document.createRange();
+ startRange.setStart(startContent.firstChild, 10);
+ startRange.setEnd(startContent.firstChild, 11);
+
+ const endRange = document.createRange();
+ endRange.setStart(endContent.lastChild, 6);
+ endRange.setEnd(endContent.lastChild, 7);
+
+ const getRangeAtStub = sandbox.stub();
+ getRangeAtStub
+ .onFirstCall().returns(startRange)
+ .onSecondCall().returns(endRange);
+ sandbox.stub(window, 'getSelection').returns({
+ rangeCount: 2,
+ getRangeAt: getRangeAtStub,
+ removeAllRanges: sandbox.stub(),
+ });
+ element._handleSelection();
+ assert.isTrue(element.isRangeSelected());
+ assert.deepEqual(getActionRange(), {
+ startLine: 119,
+ startChar: 10,
+ endLine: 120,
+ endChar: 36,
+ });
+ });
+
test('multiline grow end highlight over tabs', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
@@ -496,6 +527,7 @@
result = GrRangeNormalizer._getTextOffset(content, child);
assert.equal(result, 0);
});
+
// TODO (viktard): Selection starts in line number.
// TODO (viktard): Empty lines in selection start.
// TODO (viktard): Empty lines in selection end.
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 1b918db..c34f00f 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
@@ -57,6 +57,7 @@
type: Object,
notify: true,
value() { return {}; },
+ observer: '_changeViewStatehanged',
},
_patchRange: Object,
@@ -120,6 +121,7 @@
observers: [
'_getProjectConfig(_change.project)',
'_getFiles(_changeNum, _patchRange.*)',
+ '_setReviewedObserver(_loggedIn, params.*)',
],
keyBindings: {
@@ -141,21 +143,7 @@
attached() {
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
- if (loggedIn) {
- this._setReviewed(true);
- }
});
- if (this.changeViewState.diffMode === null) {
- // If screen size is small, always default to unified view.
- this.$.restAPI.getPreferences().then(prefs => {
- this.set('changeViewState.diffMode', prefs.default_diff_view);
- });
- }
-
- if (this._path) {
- this.fire('title-change',
- {title: this._computeFileDisplayName(this._path)});
- }
this.$.cursor.push('diffs', this.$.diff);
},
@@ -476,6 +464,21 @@
});
},
+ _changeViewStatehanged(changeViewState) {
+ if (changeViewState.diffMode === null) {
+ // If screen size is small, always default to unified view.
+ this.$.restAPI.getPreferences().then(prefs => {
+ this.set('changeViewState.diffMode', prefs.default_diff_view);
+ });
+ }
+ },
+
+ _setReviewedObserver(_loggedIn) {
+ if (_loggedIn) {
+ this._setReviewed(true);
+ }
+ },
+
/**
* If the URL hash is a diff address then configure the diff cursor.
*/
@@ -492,14 +495,15 @@
},
_pathChanged(path) {
+ if (path) {
+ this.fire('title-change',
+ {title: this._computeFileDisplayName(path)});
+ }
+
if (this._fileList.length == 0) { return; }
this.set('changeViewState.selectedFileIndex',
this._fileList.indexOf(path));
-
- if (this._loggedIn) {
- this._setReviewed(true);
- }
},
_getDiffURL(changeNum, patchRange, path) {
@@ -522,6 +526,7 @@
_computeAvailablePatches(revisions) {
const patchNums = [];
+ if (!revisions) { return patchNums; }
for (const rev of Object.values(revisions)) {
patchNums.push(rev._number);
}
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 13d7140..0a7ed28 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
@@ -54,6 +54,8 @@
getDiffChangeDetail() { return Promise.resolve(null); },
getChangeFiles() { return Promise.resolve({}); },
saveFileReviewed() { return Promise.resolve(); },
+ getDiffRobotComments() { return Promise.resolve(); },
+ getDiffDrafts() { return Promise.resolve(); },
});
element = fixture('basic');
});
@@ -469,16 +471,21 @@
});
test('file review status', done => {
- element._loggedIn = true;
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: '1',
- patchNum: '2',
- };
- element._fileList = ['/COMMIT_MSG'];
- element._path = '/COMMIT_MSG';
+ stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ });
const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
() => Promise.resolve());
+ sandbox.stub(element.$.diff, 'reload');
+
+ element._loggedIn = true;
+ element.params = {
+ view: 'gr-diff-view',
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
flush(() => {
const commitMsg = Polymer.dom(element.root).querySelector(
@@ -625,7 +632,6 @@
suite('_loadCommentMap', () => {
test('empty', done => {
stub('gr-rest-api-interface', {
- getDiffRobotComments() { return Promise.resolve({}); },
getDiffComments() { return Promise.resolve({}); },
});
element._loadCommentMap().then(map => {
@@ -636,7 +642,6 @@
test('paths in patch range', done => {
stub('gr-rest-api-interface', {
- getDiffRobotComments() { return Promise.resolve({}); },
getDiffComments() {
return Promise.resolve({
'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
@@ -658,7 +663,6 @@
test('empty for paths outside patch range', done => {
stub('gr-rest-api-interface', {
- getDiffRobotComments() { return Promise.resolve({}); },
getDiffComments() {
return Promise.resolve({
'path/to/file/one.cpp': [{patch_set: 'PARENT', message: 'lorem'}],
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 5319ace..6aae8d1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -142,20 +142,6 @@
.content.remove {
background-color: var(--light-remove-highlight-color);
}
- .dueToRebase .content.add .intraline,
- .delta.total.dueToRebase .content.add {
- background-color: var(--dark-rebased-add-highlight-color);
- }
- .dueToRebase .content.add {
- background-color: var(--light-rebased-add-highlight-color);
- }
- .dueToRebase .content.remove .intraline,
- .delta.total.dueToRebase .content.remove {
- background-color: var(--dark-rebased-remove-highlight-color);
- }
- .dueToRebase .content.remove {
- background-color: var(--light-rebased-remove-highlight-color);
- }
.content .contentText:after {
/* Newline, to ensure all lines are one line-height tall. */
content: '\A';
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 68f0294..754ca69d 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,6 +15,13 @@
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/polymer-resin/standalone/polymer-resin.html">
+<script>
+ security.polymer_resin.install({
+ allowedIdentifierPrefixes: [''],
+ reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
+ });
+</script>
<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
index a3c44e2..ad4e2b0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<dom-module id="gr-plugin-host">
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 cd7059c..efad106 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
@@ -24,6 +24,10 @@
},
},
+ behaviors: [
+ Gerrit.BaseUrlBehavior,
+ ],
+
_configChanged(config) {
const jsPlugins = config.js_resource_paths || [];
const htmlPlugins = config.html_resource_paths || [];
@@ -33,23 +37,32 @@
},
_importHtmlPlugins(plugins) {
- for (let url of plugins) {
- if (!url.startsWith('http')) {
- url = '/' + url;
- }
+ for (const url of plugins) {
this.importHref(
- url, Gerrit._pluginInstalled, Gerrit._pluginInstalled, true);
+ this._urlFor(url), Gerrit._pluginInstalled, Gerrit._pluginInstalled,
+ true);
}
},
_loadJsPlugins(plugins) {
- for (let i = 0; i < plugins.length; i++) {
- const scriptEl = document.createElement('script');
- scriptEl.defer = true;
- scriptEl.src = '/' + plugins[i];
- scriptEl.onerror = Gerrit._pluginInstalled;
- document.body.appendChild(scriptEl);
+ for (const url of plugins) {
+ this._createScriptTag(this._urlFor(url));
}
},
+
+ _createScriptTag(url) {
+ const el = document.createElement('script');
+ el.defer = true;
+ el.src = url;
+ el.onerror = Gerrit._pluginInstalled;
+ return document.body.appendChild(el);
+ },
+
+ _urlFor(pathOrUrl) {
+ if (pathOrUrl.startsWith('http')) {
+ return pathOrUrl;
+ }
+ return this.getBaseUrl() + '/' + pathOrUrl;
+ },
});
})();
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 1e81092..ead0781 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
@@ -54,7 +54,7 @@
assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
});
- test('imports html plugins from config', () => {
+ test('imports relative html plugins from config', () => {
element.config = {
html_resource_paths: ['foo/bar', 'baz'],
};
@@ -63,5 +63,73 @@
assert.isTrue(element.importHref.calledWith(
'/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
});
+
+ test('imports relative html plugins from config with a base url', () => {
+ sandbox.stub(element, 'getBaseUrl').returns('/the-base');
+ element.config = {
+ html_resource_paths: ['foo/bar', 'baz'],
+ };
+ assert.isTrue(element.importHref.calledWith(
+ '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
+ true));
+ assert.isTrue(element.importHref.calledWith(
+ '/the-base/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
+ true));
+ });
+
+ test('imports absolute html plugins from config', () => {
+ element.config = {
+ html_resource_paths: [
+ 'http://example.com/foo/bar',
+ 'https://example.com/baz',
+ ],
+ };
+ assert.isTrue(element.importHref.calledWith(
+ 'http://example.com/foo/bar', Gerrit._pluginInstalled,
+ Gerrit._pluginInstalled, true));
+ assert.isTrue(element.importHref.calledWith(
+ 'https://example.com/baz', Gerrit._pluginInstalled,
+ Gerrit._pluginInstalled, true));
+ });
+
+ test('adds js plugins from config to the body', () => {
+ element.config = {
+ js_resource_paths: ['foo/bar', 'baz'],
+ };
+ assert.isTrue(document.body.appendChild.calledTwice);
+ });
+
+ test('imports relative js plugins from config', () => {
+ sandbox.stub(element, '_createScriptTag');
+ element.config = {
+ js_resource_paths: ['foo/bar', 'baz'],
+ };
+ assert.isTrue(element._createScriptTag.calledWith('/foo/bar'));
+ assert.isTrue(element._createScriptTag.calledWith('/baz'));
+ });
+
+ test('imports relative html plugins from config with a base url', () => {
+ sandbox.stub(element, '_createScriptTag');
+ sandbox.stub(element, 'getBaseUrl').returns('/the-base');
+ element.config = {
+ js_resource_paths: ['foo/bar', 'baz'],
+ };
+ assert.isTrue(element._createScriptTag.calledWith('/the-base/foo/bar'));
+ assert.isTrue(element._createScriptTag.calledWith('/the-base/baz'));
+ });
+
+ test('imports absolute html plugins from config', () => {
+ sandbox.stub(element, '_createScriptTag');
+ element.config = {
+ js_resource_paths: [
+ 'http://example.com/foo/bar',
+ 'https://example.com/baz',
+ ],
+ };
+ assert.isTrue(element._createScriptTag.calledWith(
+ 'http://example.com/foo/bar'));
+ assert.isTrue(element._createScriptTag.calledWith(
+ 'https://example.com/baz'));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
index c93c509..e4324de 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -23,27 +23,30 @@
<dom-module id="gr-agreements-list">
<template>
<style include="shared-styles">
- .nameHeader {
- width: 15em;
+ #agreements .nameColumn {
+ min-width: 11em;
+ width: auto;
}
- .descriptionHeader {
- width: 21.5em;
+ #agreements .descriptionColumn {
+ width: auto;
}
</style>
<style include="gr-form-styles"></style>
<div class="gr-form-styles">
- <table>
+ <table id="agreements">
<thead>
<tr>
- <th class="nameHeader">Name</th>
- <th class="descriptionHeader">Description</th>
+ <th class="nameColumn">Name</th>
+ <th class="descriptionColumn">Description</th>
</tr>
</thead>
<tbody>
<template is="dom-repeat" items="[[_agreements]]">
<tr>
- <td><a href$="[[getUrlBase(item.url)]]">[[item.name]]</a></td>
- <td>[[item.description]]</td>
+ <td class="nameColumn">
+ <a href$="[[getUrlBase(item.url)]]">[[item.name]]</a>
+ </td>
+ <td class="descriptionColumn">[[item.description]]</td>
</tr>
</template>
</tbody>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index a297aa9..c93ed0c 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -24,29 +24,28 @@
<dom-module id="gr-change-table-editor">
<template>
- <style include="shared-styles">
- table {
- margin-top: 1em;
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
+ #changeCols {
+ width: auto;
}
- th.nameHeader {
- width: 11em;
+ #changeCols .visibleHeader {
+ text-align: center;
}
- td.checkboxContainer {
- border: 1px solid #fff;
+ .checkboxContainer {
cursor: pointer;
text-align: center;
}
- td.checkboxContainer:hover {
- border: 1px solid #ddd;
+ .checkboxContainer:hover {
+ outline: 1px solid #ddd;
}
</style>
- <style include="gr-form-styles"></style>
<div class="gr-form-styles">
- <table>
+ <table id="changeCols">
<thead>
<tr>
<th class="nameHeader">Column</th>
- <th>Visible</th>
+ <th class="visibleHeader">Visible</th>
</tr>
</thead>
<tbody>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
index 68d9576..99a0392 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -22,35 +22,37 @@
<dom-module id="gr-email-editor">
<template>
- <style include="shared-styles">
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
th {
color: #666;
text-align: left;
}
- th.emailHeader {
- width: 32.5em;
+ #emailTable .emailColumn {
+ min-width: 32.5em;
+ width: auto;
}
- th.preferredHeader {
+ #emailTable .preferredHeader {
text-align: center;
width: 6em;
}
- tbody tr:nth-child(even) {
- background-color: #f4f4f4;
- }
- td.preferredControl {
+ #emailTable .preferredControl {
cursor: pointer;
+ height: auto;
text-align: center;
}
- td.preferredControl:hover {
- border: 1px solid #ddd;
+ #emailTable .preferredControl .preferredRadio {
+ height: auto;
+ }
+ .preferredControl:hover {
+ outline: 1px solid #d1d2d3;
}
</style>
- <style include="gr-form-styles"></style>
<div class="gr-form-styles">
- <table>
+ <table id="emailTable">
<thead>
<tr>
- <th class="emailHeader">Email</th>
+ <th class="emailColumn">Email</th>
<th class="preferredHeader">Preferred</th>
<th></th>
</tr>
@@ -58,10 +60,11 @@
<tbody>
<template is="dom-repeat" items="[[_emails]]">
<tr>
- <td>[[item.email]]</td>
+ <td class="emailColumn">[[item.email]]</td>
<td class="preferredControl" on-tap="_handlePreferredControlTap">
<input
is="iron-input"
+ class="preferredRadio"
type="radio"
on-change="_handlePreferredChange"
name="preferred"
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
index 23d062e..bcae0bf 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -21,31 +21,33 @@
<dom-module id="gr-group-list">
<template>
- <style include="shared-styles">
- .nameHeader {
- width: 15em;
- }
- .descriptionHeader {
- width: 21.5em;
- }
- .visibleCell {
- text-align: center;
- }
- </style>
- <style include="gr-form-styles"></style>
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
+ #groups .nameColumn {
+ min-width: 11em;
+ width: auto;
+ }
+ .descriptionHeader {
+ min-width: 21.5em;
+ }
+ .visibleCell {
+ text-align: center;
+ width: 6em;
+ }
+ </style>
<div class="gr-form-styles">
- <table>
+ <table id="groups">
<thead>
<tr>
<th class="nameHeader">Name</th>
<th class="descriptionHeader">Description</th>
- <th>Visible to all</th>
+ <th class="visibleCell">Visible to all</th>
</tr>
</thead>
<tbody>
<template is="dom-repeat" items="[[_groups]]">
<tr>
- <td>[[item.name]]</td>
+ <td class="nameColumn">[[item.name]]</td>
<td>[[item.description]]</td>
<td class="visibleCell">[[_computeVisibleToAll(item)]]</td>
</tr>
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 597aae9..fa3428a 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
@@ -25,21 +25,22 @@
<dom-module id="gr-menu-editor">
<template>
<style include="shared-styles">
- th.nameHeader {
- width: 11em;
+ .buttonColumn {
+ width: 2em;
}
- tbody tr:first-of-type td .move-up-button,
- tbody tr:last-of-type td .move-down-button {
+ .moveUpButton,
+ .moveDownButton {
+ width: 100%
+ }
+ tbody tr:first-of-type td .moveUpButton,
+ tbody tr:last-of-type td .moveDownButton {
display: none;
}
td.urlCell {
word-break: break-word;
}
- .newTitleInput {
- width: 10em;
- }
.newUrlInput {
- width: 23em;
+ min-width: 23em;
}
</style>
<style include="gr-form-styles"></style>
@@ -56,17 +57,17 @@
<tr>
<td>[[item.name]]</td>
<td class="urlCell">[[item.url]]</td>
- <td>
+ <td class="buttonColumn">
<gr-button
data-index="[[index]]"
on-tap="_handleMoveUpButton"
- class="move-up-button">↑</gr-button>
+ class="moveUpButton">↑</gr-button>
</td>
- <td>
+ <td class="buttonColumn">
<gr-button
data-index="[[index]]"
on-tap="_handleMoveDownButton"
- class="move-down-button">↓</gr-button>
+ class="moveDownButton">↓</gr-button>
</td>
<td>
<gr-button
@@ -81,7 +82,6 @@
<tr>
<th>
<input
- class="newTitleInput"
is="iron-input"
placeholder="New Title"
on-keydown="_handleInputKeydown"
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index 7e12483..f16ba6c 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -48,7 +48,7 @@
// The index of the first row is 0, corresponding to the array.
function move(element, index, direction) {
const selector =
- 'tr:nth-child(' + (index + 1) + ') .move-' + direction + '-button';
+ 'tr:nth-child(' + (index + 1) + ') .move' + direction + 'Button';
const button = element.$$('tbody').querySelector(selector);
MockInteractions.tap(button);
}
@@ -110,12 +110,12 @@
['first name', 'second name', 'third name']);
// Move the middle item down
- move(element, 1, 'down');
+ move(element, 1, 'Down');
assertMenuNamesEqual(element,
['first name', 'third name', 'second name']);
// Moving the bottom item down is a no-op.
- move(element, 2, 'down');
+ move(element, 2, 'Down');
assertMenuNamesEqual(element,
['first name', 'third name', 'second name']);
});
@@ -125,13 +125,13 @@
['first name', 'second name', 'third name']);
// Move the last item up twice to be the first.
- move(element, 2, 'up');
- move(element, 1, 'up');
+ move(element, 2, 'Up');
+ move(element, 1, 'Up');
assertMenuNamesEqual(element,
['third name', 'first name', 'second name']);
// Moving the top item up is a no-op.
- move(element, 0, 'up');
+ move(element, 0, 'Up');
assertMenuNamesEqual(element,
['third name', 'first name', 'second name']);
});
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 2cf3c94..ca2feb1 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
@@ -40,6 +40,9 @@
#newEmailInput {
width: 20em;
}
+ #email {
+ margin-bottom: 1em;
+ }
</style>
<style include="gr-form-styles"></style>
<style include="gr-menu-page-styles"></style>
@@ -59,9 +62,11 @@
SSH Keys
</a></li>
<li><a href="#Groups">Groups</a></li>
- <li hidden$="[[!_serverConfig.auth.contributor_agreements]]">
- <a href="#Agreements">Agreements</a>
- </li>
+ <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
+ <li>
+ <a href="#Agreements">Agreements</a>
+ </li>
+ </template>
</ul>
</gr-page-nav>
<main class="gr-form-styles">
@@ -364,12 +369,12 @@
<fieldset>
<gr-group-list id="groupList"></gr-group-list>
</fieldset>
- <div hidden$="[[!_serverConfig.auth.contributor_agreements]]">
+ <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
<h2 id="Agreements">Agreements</h2>
<fieldset>
<gr-agreements-list id="agreementsList"></gr-agreements-list>
</fieldset>
- </div>
+ </template>
</main>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
index 82d8ebe..1afd255 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -24,10 +24,8 @@
<dom-module id="gr-ssh-editor">
<template>
- <style include="shared-styles">
- .commentHeader {
- width: 27em;
- }
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
.statusHeader {
width: 4em;
}
@@ -49,22 +47,29 @@
position: absolute;
right: 2em;
}
+ #existing {
+ margin-bottom: 1em;
+ }
+ #existing .commentColumn {
+ min-width: 27em;
+ width: auto;
+ }
</style>
- <style include="gr-form-styles"></style>
<div class="gr-form-styles">
- <fieldset>
+ <fieldset id="existing">
<table>
<thead>
<tr>
- <th class="commentHeader">Comment</th>
+ <th class="commentColumn">Comment</th>
<th class="statusHeader">Status</th>
<th class="keyHeader">Public key</th>
+ <th></th>
</tr>
</thead>
<tbody>
<template is="dom-repeat" items="[[_keys]]" as="key">
<tr>
- <td>[[key.comment]]</td>
+ <td class="commentColumn">[[key.comment]]</td>
<td>[[_getStatusLabel(key.valid)]]</td>
<td>
<gr-button
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 e82c4b0c..a834ea2 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
@@ -22,42 +22,30 @@
<dom-module id="gr-watched-projects-editor">
<template>
- <style include="shared-styles">
- th.projectHeader {
- width: 11em;
- }
- th.notificationHeader {
- text-align: center;
- }
- th.notifType {
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
+ #watchedProjects .notifType {
text-align: center;
padding: 0 0.4em;
}
- td.notifControl {
+ .notifControl {
cursor: pointer;
text-align: center;
}
- td.notifControl:hover {
- border: 1px solid #ddd;
+ .notifControl:hover {
+ outline: 1px solid #ddd;
}
.projectFilter {
color: #777;
font-style: italic;
margin-left: 1em;
}
- input {
- font-size: 1em;
- }
- .newProjectInput {
- width: 10em;
- }
.newFilterInput {
- width: 26em;
+ width: 100%;
}
</style>
- <style include="gr-form-styles"></style>
<div class="gr-form-styles">
- <table>
+ <table id="watchedProjects">
<thead>
<tr>
<th class="projectHeader">Project</th>
@@ -93,7 +81,7 @@
checked$="[[_computeCheckboxChecked(project, type.key)]]">
</td>
</template>
- <td class="delete-column">
+ <td>
<gr-button
data-index$="[[projectIndex]]"
on-tap="_handleRemoveProject">Delete</gr-button>
@@ -106,7 +94,6 @@
<th>
<gr-autocomplete
id="newProject"
- class="newProjectInput"
query="[[_query]]"
threshold="3"
placeholder="Project"></gr-autocomplete>
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 f2c3bdc..a517e6a 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
@@ -76,6 +76,7 @@
id="remove"
hidden$="[[!removable]]"
hidden
+ tabindex="-1"
aria-label="Remove"
class$="remove [[_getBackgroundClass(transparentBackground)]]"
on-tap="_handleRemoveTap">×</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index 42a6f6b..79747ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -37,7 +37,7 @@
}
</style>
<span>
- <a href$="[[_computeOwnerLink(account)]]">
+ <a href$="[[_computeOwnerLink(account)]]" tabindex="-1">
<gr-account-label account="[[account]]"
avatar-image-size="[[avatarImageSize]]"
show-email="[[_computeShowEmail(account)]]"></gr-account-label>
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 94e17ea..2a86a0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -27,6 +27,7 @@
font-size: 1em;
height: 100%;
width: 100%;
+ @apply --gr-autocomplete;
}
input.borderless,
input.borderless:focus {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
index 32b6f67..4095d3e 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -16,6 +16,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-avatar">
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 166204bf..5fca577 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -28,6 +28,10 @@
},
},
+ behaviors: [
+ Gerrit.BaseUrlBehavior,
+ ],
+
created() {
this.hidden = true;
},
@@ -64,7 +68,8 @@
return avatars[i].url;
}
}
- return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
+ return this.getBaseUrl() + '/accounts/' +
+ account._account_id + '/avatar?s=' + this.imageSize;
},
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index c28679c..4a45352 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -55,10 +55,10 @@
}, REQUEST_DEBOUNCE_INTERVAL_MS);
},
- _computeNavLink(offset, direction, projectsPerPage, filter) {
+ _computeNavLink(offset, direction, itemsPerPage, filter) {
// Offset could be a string when passed from the router.
offset = +(offset || 0);
- const newOffset = Math.max(0, offset + (projectsPerPage * direction));
+ const newOffset = Math.max(0, offset + (itemsPerPage * direction));
let href = this.getBaseUrl() + this.path;
if (filter) {
href += '/q/filter:' + filter;
@@ -73,12 +73,12 @@
return offset === 0;
},
- _hideNextArrow(loading, projects) {
+ _hideNextArrow(loading, items) {
let lastPage = false;
- if (projects.length < this.itemsPerPage + 1) {
+ if (items.length < this.itemsPerPage + 1) {
lastPage = true;
}
- return loading || lastPage || !projects || !projects.length;
+ return loading || lastPage || !items || !items.length;
},
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
index b7090da..3f954b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
@@ -40,6 +40,9 @@
border-top: 1px solid transparent;
padding: 0 2em;
}
+ #nav ::content li a {
+ word-break: break-all;
+ }
#nav ::content .subsectionItem {
padding-left: 3em;
}
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 eb2b8c6..4f83086 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
@@ -523,6 +523,16 @@
);
},
+ getProjectBranches(filter, project, projectsBranchesPerPage, opt_offset) {
+ const offset = opt_offset || 0;
+ filter = filter ? '&m=' + filter : '';
+
+ return this._fetchSharedCacheURL(
+ `/projects/${project}/branches?n=${projectsBranchesPerPage + 1}&s=` +
+ `${offset}${filter}`
+ );
+ },
+
getPlugins() {
return this._fetchSharedCacheURL('/plugins/?all');
},
diff --git a/polygerrit-ui/app/embed/change-diff-views.html b/polygerrit-ui/app/embed/change-diff-views.html
index b4f521b..8426585 100644
--- a/polygerrit-ui/app/embed/change-diff-views.html
+++ b/polygerrit-ui/app/embed/change-diff-views.html
@@ -16,3 +16,4 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../elements/change/gr-change-view/gr-change-view.html">
<link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="../styles/app-theme.html">
diff --git a/polygerrit-ui/app/embed/embed_test.html b/polygerrit-ui/app/embed/embed_test.html
new file mode 100644
index 0000000..26ea895
--- /dev/null
+++ b/polygerrit-ui/app/embed/embed_test.html
@@ -0,0 +1,51 @@
+<!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>change-diff-views-embed_test</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="../polygerrit_ui/elements/change-diff-views.html"/>
+
+<script>void(0);</script>
+
+<test-fixture id="change-view">
+ <template>
+ <gr-change-view></gr-change-view>
+ </template>
+</test-fixture>
+
+<test-fixture id="diff-view">
+ <template>
+ <gr-diff-view></gr-diff-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('embed test', () => {
+ test('gr-change-view is embedded', () => {
+ const element = fixture('change-view');
+ assert.equal(element.is, 'gr-change-view');
+ });
+
+ test('diff-view is embedded', () => {
+ const element = fixture('diff-view');
+ assert.equal(element.is, 'gr-diff-view');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/embed/test.html b/polygerrit-ui/app/embed/test.html
new file mode 100644
index 0000000..0587562
--- /dev/null
+++ b/polygerrit-ui/app/embed/test.html
@@ -0,0 +1,25 @@
+<!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>Embed Test Runner</title>
+<meta charset="utf-8">
+<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../bower_components/web-component-tester/browser.js"></script>
+<script>
+ WCT.loadSuites(['embed_test.html']);
+</script>
diff --git a/polygerrit-ui/app/embed_test.sh b/polygerrit-ui/app/embed_test.sh
new file mode 100755
index 0000000..adcc653
--- /dev/null
+++ b/polygerrit-ui/app/embed_test.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+set -ex
+
+t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX)
+components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip
+code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/polygerrit_embed_ui.zip
+index=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html
+tests=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/*_test.html
+
+unzip -qd $t $components
+unzip -qd $t $code
+mkdir -p $t/test
+cp $index $t/test/
+cp $tests $t/test/
+
+# For some reason wct tries to install selenium into its node_modules
+# directory on first run. If you've installed into /usr/local and
+# aren't running wct as root, you're screwed. Turning this option off
+# through skipSeleniumInstall seems to still work, so there's that.
+
+# Sauce tests are disabled by default in order to run local tests
+# only. Run it with (saucelabs.com account required; free for open
+# source): WCT_ARGS='--plugin sauce' ./polygerrit-ui/app/embed_test.sh
+
+cat <<EOF > $t/wct.conf.js
+module.exports = {
+ 'suites': ['test'],
+ 'webserver': {
+ 'pathMappings': [
+ {'/components/bower_components': 'bower_components'}
+ ]
+ },
+ 'plugins': {
+ 'local': {
+ 'skipSeleniumInstall': true
+ },
+ 'sauce': {
+ 'disabled': true,
+ 'browsers': [
+ 'OS X 10.12/chrome',
+ 'Windows 10/chrome',
+ 'Linux/firefox',
+ 'OS X 10.12/safari',
+ 'Windows 10/microsoftedge'
+ ]
+ }
+ }
+ };
+EOF
+
+export PATH="$(dirname $WCT):$(dirname $NPM):$PATH"
+
+cd $t
+test -n "${WCT}"
+
+$(basename ${WCT}) ${WCT_ARGS}
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 5e9cde8..46fc46b 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -29,6 +29,14 @@
<link rel="stylesheet" href="/styles/fonts.css">
<link rel="stylesheet" href="/styles/main.css">
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<!--
+ - Content between webcomponents-lite and the load of the main app element
+ - run before polymer-resin is installed so may have security consequences.
+ - Contact your local security engineer if you have any questions, and
+ - CC them on any changes that load content before gr-app.html.
+ -
+ - github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
+ -->
<link rel="preload" href="/elements/gr-app.js" as="script" crossorigin="anonymous">
<link rel="import" href="/elements/gr-app.html">
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
new file mode 100644
index 0000000..be80c13
--- /dev/null
+++ b/polygerrit-ui/app/rules.bzl
@@ -0,0 +1,94 @@
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary")
+load(
+ "//tools/bzl:js.bzl",
+ "bower_component_bundle",
+ "vulcanize",
+ "bower_component",
+ "js_component",
+)
+
+def polygerrit_bundle(name, srcs, outs, app):
+ appName = app.split(".html")[0].split("/").pop() # eg: gr-app
+
+ closure_js_binary(
+ name = name + "_closure_bin",
+ # Known issue: Closure compilation not compatible with Polymer behaviors.
+ # See: https://github.com/google/closure-compiler/issues/2042
+ compilation_level = "WHITESPACE_ONLY",
+ defs = [
+ "--polymer_pass",
+ "--jscomp_off=duplicate",
+ "--force_inject_library=es6_runtime",
+ ],
+ language = "ECMASCRIPT5",
+ deps = [name + "_closure_lib"],
+ )
+
+ closure_js_library(
+ name = name + "_closure_lib",
+ srcs = [appName + ".js"],
+ convention = "GOOGLE",
+ # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
+ # and remove this supression
+ suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"],
+ deps = [
+ "//lib/polymer_externs:polymer_closure",
+ "@io_bazel_rules_closure//closure/library",
+ ],
+ )
+
+ vulcanize(
+ name = appName,
+ srcs = srcs,
+ app = app,
+ deps = ["//polygerrit-ui:polygerrit_components.bower_components"],
+ )
+
+ native.filegroup(
+ name = name + "_app_sources",
+ srcs = [
+ name + "_closure_bin.js",
+ appName + ".html",
+ ],
+ )
+
+ native.filegroup(
+ name = name + "_css_sources",
+ srcs = native.glob(["styles/**/*.css"]),
+ )
+
+ native.filegroup(
+ name = name + "_top_sources",
+ srcs = [
+ "favicon.ico",
+ "index.html",
+ ],
+ )
+
+ genrule2(
+ name = name,
+ srcs = [
+ name + "_app_sources",
+ name + "_css_sources",
+ name + "_top_sources",
+ "//lib/fonts:robotomono",
+ "//lib/js:highlightjs_files",
+ # we extract from the zip, but depend on the component for license checking.
+ "@webcomponentsjs//:zipfile",
+ "//lib/js:webcomponentsjs"
+ ],
+ outs = outs,
+ cmd = " && ".join([
+ "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
+ "for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + appName + ".$$ext; done",
+ "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/",
+ "for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
+ "for f in $(locations "+ name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
+ "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
+ "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js",
+ "cd $$TMP",
+ "find . -exec touch -t 198001010000 '{}' ';'",
+ "zip -qr $$ROOT/$@ *",
+ ]),
+ )
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index c2421ad..0edf41c 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -21,4 +21,5 @@
--test_env="NPM=${npm_bin}" \
--test_env="DISPLAY=${DISPLAY}" \
"$@" \
+ //polygerrit-ui/app:embed_test \
//polygerrit-ui/app:wct_test
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index d8b799b..9fefc0e 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -19,17 +19,23 @@
.gr-form-styles h1 {
margin-bottom: .1em;
}
+ .gr-form-styles h2 {
+ margin-bottom: .3em;
+ }
.gr-form-styles fieldset {
border: none;
margin: 0 0 2em 2em;
}
.gr-form-styles section {
- margin-bottom: .5em;
+ margin: .15em 0;
+ min-height: 2em;
+ }
+ .gr-form-styles section * {
+ vertical-align: middle;
}
.gr-form-styles .title,
.gr-form-styles .value {
display: inline-block;
- vertical-align: top;
}
.gr-form-styles .title {
color: #666;
@@ -37,19 +43,77 @@
padding-right: .5em;
width: 11em;
}
- .gr-form-styles input {
- font-size: 1em;
- }
.gr-form-styles iron-autogrow-textarea {
font-size: 1em;
}
.gr-form-styles th {
color: #666;
text-align: left;
+ vertical-align: bottom;
+ }
+ .gr-form-styles td,
+ .gr-form-styles tfoot th {
+ height: 2em;
+ vertical-align: middle;
+ }
+ .gr-form-styles .emptyHeader {
+ text-align: right;
}
.gr-form-styles tbody tr:nth-child(even) {
background-color: #f4f4f4;
}
+ .gr-form-styles table {
+ width: 50em;
+ }
+ .gr-form-styles th:first-child,
+ .gr-form-styles td:first-child {
+ width: 11em;
+ }
+ .gr-form-styles th:first-child input,
+ .gr-form-styles td:first-child input {
+ width: 10em;
+ }
+ .gr-form-styles input:not([type="checkbox"]),
+ .gr-form-styles select,
+ .gr-form-styles textarea {
+ border: 1px solid #d1d2d3;
+ border-radius: 2px;
+ font-size: 1em;
+ height: 2em;
+ padding: 0 .15em;
+ }
+ .gr-form-styles gr-button:not([link]) {
+ height: 2.2em;
+ }
+ .gr-form-styles td:last-child {
+ width: 5em;
+ }
+ .gr-form-styles th:last-child gr-button,
+ .gr-form-styles td:last-child gr-button {
+ width: 100%;
+ }
+ .gr-form-styles iron-autogrow-textarea {
+ border: none;
+ height: auto;
+ min-height: 2em;
+ --iron-autogrow-textarea: {
+ border: 1px solid #d1d2d3;
+ border-radius: 2px;
+ font-size: 1em;
+ padding: .25em .15em 0 .15em;
+ }
+ }
+ .gr-form-styles gr-autocomplete {
+ border: none;
+ --gr-autocomplete: {
+ border: 1px solid #d1d2d3;
+ border-radius: 2px;
+ font-size: 1em;
+ height: 2em;
+ padding: 0 .15em;
+ width: 10em;
+ }
+ }
@media only screen and (max-width: 40em) {
.gr-form-styles section {
margin-bottom: 1em;
@@ -58,6 +122,9 @@
.gr-form-styles .value {
display: block;
}
+ .gr-form-styles table {
+ width: 100%;
+ }
}
</style>
</template>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index fb24af2d..7c894b6 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -15,5 +15,24 @@
limitations under the License.
-->
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
+<link rel="import"
+ href="../bower_components/polymer-resin/standalone/polymer-resin.html" />
+<script>
+ security.polymer_resin.install({
+ allowedIdentifierPrefixes: [''],
+ reportHandler(isViolation, fmt, ...args) {
+ const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
+ log(isViolation, fmt, ...args);
+ if (isViolation) {
+ // This will cause the test to fail if there is a data binding
+ // violation.
+ throw new Error(
+ 'polymer-resin violation: ' + fmt
+ + JSON.stringify(args));
+ }
+ },
+ });
+</script>
+<link rel="import"
+ href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
<link rel="import" href="test-router.html" />
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 7d29db5..f31e9be 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -36,6 +36,7 @@
'admin/gr-admin-project/gr-admin-project_test.html',
'admin/gr-admin-project-list/gr-admin-project-list_test.html',
'admin/gr-admin-view/gr-admin-view_test.html',
+ 'admin/gr-project-branches/gr-project-branches_test.html',
'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',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index f2a97bf..a42acc3 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -122,6 +122,9 @@
}
func injectLocalPlugins(r io.Reader) io.Reader {
+ if len(*plugins) == 0 {
+ return r
+ }
// Skip escape prefix
io.CopyN(ioutil.Discard, r, 5)
dec := json.NewDecoder(r)