Merge "PolyGerrit: Add some more missing configs to gr-project"
diff --git a/.gitignore b/.gitignore
index 06a6e66..0e954ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@
/plugins/cookbook-plugin/
/test_site
/tools/format
+/.vscode
\ No newline at end of file
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 2c5acc2..73fd687 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -740,6 +740,9 @@
* `"diff"`: default is `10m` (10 MiB of memory)
* `"diff_intraline"`: default is `10m` (10 MiB of memory)
* `"diff_summary"`: default is `10m` (10 MiB of memory)
+* `"groups"`: default is unlimited
+* `"groups_byname"`: default is unlimited
+* `"groups_byuuid"`: default is unlimited
* `"plugin_resources"`: default is 2m (2 MiB of memory)
+
@@ -841,9 +844,40 @@
cache `"groups"`::
+
-Caches the basic group information from the `account_groups` table,
+Caches the basic group information of internal groups by group ID,
including the group owner, name, and description.
+
+For this cache it is important to configure a size that is larger than
+the number of internal Gerrit groups, otherwise general Gerrit
+performance may be poor. This is why by default this cache is
+unlimited.
++
+External group membership obtained from LDAP is cached under
+`"ldap_groups"`.
+
+cache `"groups_byname"`::
++
+Caches the basic group information of internal groups by group name,
+including the group owner, name, and description.
++
+For this cache it is important to configure a size that is larger than
+the number of internal Gerrit groups, otherwise general Gerrit
+performance may be poor. This is why by default this cache is
+unlimited.
++
+External group membership obtained from LDAP is cached under
+`"ldap_groups"`.
+
+cache `"groups_byuuid"`::
++
+Caches the basic group information of internal groups by group UUID,
+including the group owner, name, and description.
++
+For this cache it is important to configure a size that is larger than
+the number of internal Gerrit groups, otherwise general Gerrit
+performance may be poor. This is why by default this cache is
+unlimited.
++
External group membership obtained from LDAP is cached under
`"ldap_groups"`.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index dada623..8774ed2 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2249,7 +2249,7 @@
link:rest-api-accounts.html#create-account[account creation] REST API and
inject additional external identifiers for an account that represents a user
in some external user store. For that, an implementation of the extension
-point `com.google.gerrit.server.api.accounts.AccountExternalIdCreator`
+point `com.google.gerrit.server.account.AccountExternalIdCreator`
must be registered.
[source,java]
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 66c67dc..c4017f3 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -42,7 +42,7 @@
Basically, the DOM is the API surface. Low-level API provides methods for
decorating, replacing, and styling DOM elements exposed through a set of
-endpoints.
+link:pg-plugin-endpoints.html[endpoints].
PolyGerrit provides a simple way for accessing the DOM via DOM hooks API. A DOM
hook is a custom element that is instantiated for the plugin endpoint. In the
@@ -70,8 +70,6 @@
For each endpoint, PolyGerrit provides a list of DOM properties (such as
attributes and events) that are supported in the long-term.
-NOTE: TODO: Insert link to the full endpoints API.
-
``` js
Gerrit.install(plugin => {
const domHook = plugin.hook('reply-text');
@@ -159,6 +157,7 @@
=== hook
`plugin.hook(endpointName, opt_options)`
+See list of supported link:pg-plugin-endpoints.html[endpoints].
Note: TODO
=== registerCustomComponent
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
new file mode 100644
index 0000000..7c960dd
--- /dev/null
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -0,0 +1,69 @@
+= Gerrit Code Review - PolyGerrit Plugin Styling
+
+Plugin should be html-based and imported following PolyGerrit's
+link:pg-plugin-dev.html#loading[dev guide].
+
+Sample code for testing endpoints:
+
+``` js
+Gerrit.install(plugin => {
+ // Change endpoint below
+ const endpoint = 'change-metadata-item';
+ plugin.hook(endpoint).onAttached(element => {
+ console.log(endpoint, element);
+ const el = element.appendChild(document.createElement('div'));
+ el.textContent = 'Ah, there it is. Lovely.';
+ el.style = 'background: pink; line-height: 4em; text-align: center;';
+ });
+});
+```
+
+== Default parameters
+All endpoints receive the following params, set as attributes to custom components
+that are instantiated at the endpoint:
+
+* `plugin`
++
+the current plugin instance, the one that is used by `Gerrit.install()`.
+
+* `content`
++
+decorated DOM Element, is only set for registrations that decorate existing
+components.
+
+== Plugin endpoints
+
+Following endpoints are available to plugins
+
+=== change-view-integration
+Extension point is located between `Files` and `Messages` section on the change
+view page, and it may take full page's width. Primary purpose is to enable
+plugins to display custom CI related information (build status, etc).
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
+=== change-metadata-item
+Extension point is located on the bottom of the change view left panel, under
+`Label Status` and `Links` sections. It's width is equal to the left panel's and
+primary purpose is to enable plugins to add sections of metadata to the left
+panel.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 5c1a29e..2cc78a4 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -324,6 +324,33 @@
}
----
+All::
+Get all projects, including those whose state is "HIDDEN".
++
+.Request
+----
+GET /projects/?all HTTP/1.0
+----
++
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "All-Projects" {
+ "id": "All-Projects",
+ "state": "ACTIVE"
+ },
+ "some-other-project": {
+ "id": "some-other-project",
+ "state": "HIDDEN"
+ }
+ }
+----
+
[[query-projects]]
=== Query Projects
--
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8d50073..9566e0f 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -39,12 +39,14 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.LabelFunction;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -80,6 +82,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.change.Abandon;
import com.google.gerrit.server.change.ChangeResource;
@@ -220,6 +223,7 @@
@Inject protected FakeEmailSender sender;
@Inject protected GerritApi gApi;
@Inject protected GitRepositoryManager repoManager;
+ @Inject protected GroupBackend groupBackend;
@Inject protected GroupCache groupCache;
@Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
@Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
@@ -346,9 +350,9 @@
// later on. As test indexes are non-permanent, closing an instance and opening another one
// removes all indexed data.
// As a workaround, we simply reindex all available groups here.
- Iterable<AccountGroup.UUID> allGroupUuids = groups.getAllUuids(db)::iterator;
- for (AccountGroup.UUID groupUuid : allGroupUuids) {
- groupCache.onCreateGroup(groupUuid);
+ Iterable<GroupReference> allGroups = groups.getAllGroupReferences(db)::iterator;
+ for (GroupReference group : allGroups) {
+ groupCache.onCreateGroup(group.getUUID());
}
admin = accountCreator.admin();
@@ -917,9 +921,7 @@
protected void grant(Project.NameKey project, String ref, String permission, boolean force)
throws RepositoryNotFoundException, IOException, ConfigInvalidException {
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- grant(project, ref, permission, force, adminGroup.getGroupUUID());
+ grant(project, ref, permission, force, adminGroupUuid());
}
protected void grant(
@@ -1121,8 +1123,7 @@
String g = createGroup("cla-test-group");
GroupApi groupApi = gApi.groups().id(g);
groupApi.description("CLA test group");
- InternalGroup caGroup =
- groupCache.get(new AccountGroup.UUID(groupApi.detail().id)).orElse(null);
+ InternalGroup caGroup = group(new AccountGroup.UUID(groupApi.detail().id));
GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
PermissionRule rule = new PermissionRule(groupRef);
rule.setAction(PermissionRule.Action.ALLOW);
@@ -1290,6 +1291,113 @@
}
}
+ protected void assertPermissions(
+ Project.NameKey project,
+ GroupReference groupReference,
+ String ref,
+ boolean exclusive,
+ String... permissionNames)
+ throws IOException {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ AccessSection accessSection = cfg.getAccessSection(ref);
+ assertThat(accessSection).isNotNull();
+ for (String permissionName : permissionNames) {
+ Permission permission = accessSection.getPermission(permissionName);
+ assertPermission(permission, permissionName, exclusive, null);
+ assertPermissionRule(
+ permission.getRule(groupReference), groupReference, Action.ALLOW, false, 0, 0);
+ }
+ }
+
+ protected void assertLabelPermission(
+ Project.NameKey project,
+ GroupReference groupReference,
+ String ref,
+ boolean exclusive,
+ String labelName,
+ int min,
+ int max)
+ throws IOException {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ AccessSection accessSection = cfg.getAccessSection(ref);
+ assertThat(accessSection).isNotNull();
+
+ String permissionName = Permission.LABEL + labelName;
+ Permission permission = accessSection.getPermission(permissionName);
+ assertPermission(permission, permissionName, exclusive, labelName);
+ assertPermissionRule(
+ permission.getRule(groupReference), groupReference, Action.ALLOW, false, min, max);
+ }
+
+ private void assertPermission(
+ Permission permission,
+ String expectedName,
+ boolean expectedExclusive,
+ @Nullable String expectedLabelName) {
+ assertThat(permission).isNotNull();
+ assertThat(permission.getName()).isEqualTo(expectedName);
+ assertThat(permission.getExclusiveGroup()).isEqualTo(expectedExclusive);
+ assertThat(permission.getLabel()).isEqualTo(expectedLabelName);
+ }
+
+ private void assertPermissionRule(
+ PermissionRule rule,
+ GroupReference expectedGroupReference,
+ Action expectedAction,
+ boolean expectedForce,
+ int expectedMin,
+ int expectedMax) {
+ assertThat(rule.getGroup()).isEqualTo(expectedGroupReference);
+ assertThat(rule.getAction()).isEqualTo(expectedAction);
+ assertThat(rule.getForce()).isEqualTo(expectedForce);
+ assertThat(rule.getMin()).isEqualTo(expectedMin);
+ assertThat(rule.getMax()).isEqualTo(expectedMax);
+ }
+
+ protected InternalGroup group(AccountGroup.UUID groupUuid) {
+ InternalGroup group = groupCache.get(groupUuid).orElse(null);
+ assertThat(group).named(groupUuid.get()).isNotNull();
+ return group;
+ }
+
+ protected GroupReference groupRef(AccountGroup.UUID groupUuid) {
+ GroupDescription.Basic groupDescription = groupBackend.get(groupUuid);
+ return new GroupReference(groupDescription.getGroupUUID(), groupDescription.getName());
+ }
+
+ protected InternalGroup group(String groupName) {
+ InternalGroup group = groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null);
+ assertThat(group).named(groupName).isNotNull();
+ return group;
+ }
+
+ protected GroupReference groupRef(String groupName) {
+ InternalGroup group = groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null);
+ assertThat(group).isNotNull();
+ return new GroupReference(group.getGroupUUID(), group.getName());
+ }
+
+ protected AccountGroup.UUID groupUuid(String groupName) {
+ return group(groupName).getGroupUUID();
+ }
+
+ protected InternalGroup adminGroup() {
+ return group("Administrators");
+ }
+
+ protected GroupReference adminGroupRef() {
+ return groupRef("Administrators");
+ }
+
+ protected AccountGroup.UUID adminGroupUuid() {
+ return groupUuid("Administrators");
+ }
+
+ protected void assertGroupDoesNotExist(String groupName) {
+ InternalGroup group = groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null);
+ assertThat(group).named(groupName).isNull();
+ }
+
protected void assertNotifyTo(TestAccount expected) {
assertNotifyTo(expected.emailAddress);
}
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 636e909..b29922f 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -34,6 +34,7 @@
import com.google.gerrit.server.util.SocketUtil;
import com.google.gerrit.server.util.SystemLog;
import com.google.gerrit.testing.FakeEmailSender;
+import com.google.gerrit.testing.GroupNoteDbMode;
import com.google.gerrit.testing.NoteDbChecker;
import com.google.gerrit.testing.NoteDbMode;
import com.google.gerrit.testing.SshMode;
@@ -391,6 +392,7 @@
cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
NoteDbMode.newNotesMigrationFromEnv().setConfigValues(cfg);
+ GroupNoteDbMode.get().getGroupsMigration().setConfigValuesIfNotSetYet(cfg);
}
private static Injector createTestInjector(Daemon daemon) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 3f2cbda..4b1211b 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.config.TrackingFootersProvider;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.schema.DataSourceType;
@@ -78,6 +79,7 @@
bind(DataSourceType.class).to(InMemoryH2Type.class);
install(new NotesMigration.Module());
+ install(new GroupsMigration.Module());
TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
new TypeLiteral<SchemaFactory<ReviewDb>>() {};
bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 5f3c1ec..919f532 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -35,7 +35,6 @@
["**/*.java"],
exclude = ANNOTATIONS,
),
- resources = [":Version"],
visibility = ["//visibility:public"],
deps = [
":annotations",
@@ -52,6 +51,15 @@
],
)
+# ":version" should not be in the dependency graph of the acceptance
+# tests to avoid spurious test re-runs. That's because the content of
+# //:version.txt is changed when the outcome of `git describe` is changed.
+java_library(
+ name = "version",
+ resources = [":Version"],
+ visibility = ["//visibility:public"],
+)
+
genrule(
name = "gen_version",
srcs = ["//:version.txt"],
diff --git a/java/com/google/gerrit/common/TimeUtil.java b/java/com/google/gerrit/common/TimeUtil.java
index b1697dc..7f53f84 100644
--- a/java/com/google/gerrit/common/TimeUtil.java
+++ b/java/com/google/gerrit/common/TimeUtil.java
@@ -18,6 +18,10 @@
import com.google.common.annotations.VisibleForTesting;
import java.sql.Timestamp;
import java.util.function.LongSupplier;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
/** Static utility methods for dealing with dates and times. */
@GwtIncompatible("Unemulated Java 8 functionalities")
@@ -36,18 +40,67 @@
return new Timestamp(nowMs());
}
- public static Timestamp roundToSecond(Timestamp t) {
+ public static Timestamp truncateToSecond(Timestamp t) {
return new Timestamp((t.getTime() / 1000) * 1000);
}
@VisibleForTesting
public static void setCurrentMillisSupplier(LongSupplier customCurrentMillisSupplier) {
currentMillisSupplier = customCurrentMillisSupplier;
+
+ SystemReader oldSystemReader = SystemReader.getInstance();
+ if (!(oldSystemReader instanceof GerritSystemReader)) {
+ SystemReader.setInstance(new GerritSystemReader(oldSystemReader));
+ }
}
@VisibleForTesting
public static void resetCurrentMillisSupplier() {
currentMillisSupplier = SYSTEM_CURRENT_MILLIS_SUPPLIER;
+ SystemReader.setInstance(null);
+ }
+
+ private static class GerritSystemReader extends SystemReader {
+ SystemReader delegate;
+
+ GerritSystemReader(SystemReader delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String getHostname() {
+ return delegate.getHostname();
+ }
+
+ @Override
+ public String getenv(String variable) {
+ return delegate.getenv(variable);
+ }
+
+ @Override
+ public String getProperty(String key) {
+ return delegate.getProperty(key);
+ }
+
+ @Override
+ public FileBasedConfig openUserConfig(Config parent, FS fs) {
+ return delegate.openUserConfig(parent, fs);
+ }
+
+ @Override
+ public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+ return delegate.openSystemConfig(parent, fs);
+ }
+
+ @Override
+ public long getCurrentTime() {
+ return currentMillisSupplier.getAsLong();
+ }
+
+ @Override
+ public int getTimezone(long when) {
+ return delegate.getTimezone(when);
+ }
}
private TimeUtil() {}
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 8f0ea8f..3c471ac 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -14,39 +14,71 @@
package com.google.gerrit.elasticsearch;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.codec.binary.Base64.decodeBase64;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
import com.google.common.collect.Streams;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.Schema.Values;
+import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import io.searchbox.client.JestResult;
import io.searchbox.client.http.JestHttpClient;
import io.searchbox.core.Bulk;
import io.searchbox.core.Delete;
+import io.searchbox.core.Search;
+import io.searchbox.core.search.sort.Sort;
import io.searchbox.indices.CreateIndex;
import io.searchbox.indices.DeleteIndex;
import io.searchbox.indices.IndicesExists;
import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Function;
+import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.lib.Config;
import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
+ private static final Logger log = LoggerFactory.getLogger(AbstractElasticIndex.class);
+
protected static <T> List<T> decodeProtos(
JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
JsonArray field = doc.getAsJsonArray(fieldName);
@@ -146,7 +178,7 @@
protected io.searchbox.core.Index insert(String type, V v) throws IOException {
String id = getId(v);
- String doc = toDoc(v);
+ String doc = toDocument(v);
return new io.searchbox.core.Index.Builder(doc).index(indexName).type(type).id(id).build();
}
@@ -154,7 +186,7 @@
return !(element instanceof String) || !((String) element).isEmpty();
}
- private String toDoc(V v) throws IOException {
+ private String toDocument(V v) throws IOException {
XContentBuilder builder = jsonBuilder().startObject();
for (Values<V> values : schema.buildFields(v)) {
String name = values.getField().getName();
@@ -171,4 +203,129 @@
}
return builder.endObject().string();
}
+
+ protected abstract V fromDocument(JsonObject doc, Set<String> fields);
+
+ protected FieldBundle toFieldBundle(JsonObject doc) {
+ Map<String, FieldDef<V, ?>> allFields = getSchema().getFields();
+ ListMultimap<String, Object> rawFields = ArrayListMultimap.create();
+ for (Entry<String, JsonElement> element : doc.get("fields").getAsJsonObject().entrySet()) {
+ checkArgument(
+ allFields.containsKey(element.getKey()), "Unrecognized field " + element.getKey());
+ FieldType<?> type = allFields.get(element.getKey()).getType();
+
+ Iterable<JsonElement> innerItems =
+ element.getValue().isJsonArray()
+ ? element.getValue().getAsJsonArray()
+ : Collections.singleton(element.getValue());
+
+ for (JsonElement inner : innerItems) {
+ if (type == FieldType.EXACT || type == FieldType.FULL_TEXT || type == FieldType.PREFIX) {
+ rawFields.put(element.getKey(), inner.getAsString());
+ } else if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
+ rawFields.put(element.getKey(), inner.getAsInt());
+ } else if (type == FieldType.LONG) {
+ rawFields.put(element.getKey(), inner.getAsLong());
+ } else if (type == FieldType.TIMESTAMP) {
+ rawFields.put(element.getKey(), new Timestamp(inner.getAsLong()));
+ } else if (type == FieldType.STORED_ONLY) {
+ rawFields.put(element.getKey(), Base64.decodeBase64(inner.getAsString()));
+ } else {
+ throw FieldType.badFieldType(type);
+ }
+ }
+ }
+ return new FieldBundle(rawFields);
+ }
+
+ protected class ElasticQuerySource implements DataSource<V> {
+ private final QueryOptions opts;
+ private final Search search;
+
+ ElasticQuerySource(Predicate<V> p, QueryOptions opts, String type, Sort sort)
+ throws QueryParseException {
+ this(p, opts, ImmutableList.of(type), ImmutableList.of(sort));
+ }
+
+ ElasticQuerySource(
+ Predicate<V> p, QueryOptions opts, Collection<String> types, Collection<Sort> sorts)
+ throws QueryParseException {
+ this.opts = opts;
+ QueryBuilder qb = queryBuilder.toQueryBuilder(p);
+ SearchSourceBuilder searchSource =
+ new SearchSourceBuilder()
+ .query(qb)
+ .from(opts.start())
+ .size(opts.limit())
+ .fields(Lists.newArrayList(opts.fields()));
+
+ search =
+ new Search.Builder(searchSource.toString())
+ .addType(types)
+ .addSort(sorts)
+ .addIndex(indexName)
+ .build();
+ }
+
+ @Override
+ public int getCardinality() {
+ return 10;
+ }
+
+ @Override
+ public ResultSet<V> read() throws OrmException {
+ return readImpl((doc) -> AbstractElasticIndex.this.fromDocument(doc, opts.fields()));
+ }
+
+ @Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ return readImpl(AbstractElasticIndex.this::toFieldBundle);
+ }
+
+ @Override
+ public String toString() {
+ return search.toString();
+ }
+
+ private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) throws OrmException {
+ try {
+ List<T> results = Collections.emptyList();
+ JestResult result = client.execute(search);
+ if (result.isSucceeded()) {
+ JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
+ if (obj.get("hits") != null) {
+ JsonArray json = obj.getAsJsonArray("hits");
+ results = Lists.newArrayListWithCapacity(json.size());
+ for (int i = 0; i < json.size(); i++) {
+ T mapperResult = mapper.apply(json.get(i).getAsJsonObject());
+ if (mapperResult != null) {
+ results.add(mapperResult);
+ }
+ }
+ }
+ } else {
+ log.error(result.getErrorMessage());
+ }
+ final List<T> r = Collections.unmodifiableList(results);
+ return new ResultSet<T>() {
+ @Override
+ public Iterator<T> iterator() {
+ return r.iterator();
+ }
+
+ @Override
+ public List<T> toList() {
+ return r;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+ }
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index 18eb660..ba5178e 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -16,9 +16,7 @@
import static com.google.gerrit.server.index.account.AccountField.ID;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
@@ -33,30 +31,19 @@
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndex;
-import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.Search;
import io.searchbox.core.search.sort.Sort;
import io.searchbox.core.search.sort.Sort.Sorting;
import java.io.IOException;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.builder.SearchSourceBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class ElasticAccountIndex extends AbstractElasticIndex<Account.Id, AccountState>
implements AccountIndex {
@@ -71,8 +58,6 @@
static final String ACCOUNTS = "accounts";
static final String ACCOUNTS_PREFIX = ACCOUNTS + "_";
- private static final Logger log = LoggerFactory.getLogger(ElasticAccountIndex.class);
-
private final AccountMapping mapping;
private final Provider<AccountCache> accountCache;
@@ -109,7 +94,9 @@
@Override
public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(p, opts);
+ Sort sort = new Sort(AccountField.ID.getName(), Sorting.ASC);
+ sort.setIgnoreUnmapped();
+ return new ElasticQuerySource(p, opts.filterFields(IndexUtils::accountFields), ACCOUNTS, sort);
}
@Override
@@ -128,92 +115,18 @@
return as.getAccount().getId().toString();
}
- private class QuerySource implements DataSource<AccountState> {
- private final Search search;
- private final Set<String> fields;
-
- QuerySource(Predicate<AccountState> p, QueryOptions opts) throws QueryParseException {
- QueryBuilder qb = queryBuilder.toQueryBuilder(p);
- fields = IndexUtils.accountFields(opts);
- SearchSourceBuilder searchSource =
- new SearchSourceBuilder()
- .query(qb)
- .from(opts.start())
- .size(opts.limit())
- .fields(Lists.newArrayList(fields));
-
- Sort sort = new Sort(AccountField.ID.getName(), Sorting.ASC);
- sort.setIgnoreUnmapped();
-
- search =
- new Search.Builder(searchSource.toString())
- .addType(ACCOUNTS)
- .addIndex(indexName)
- .addSort(ImmutableList.of(sort))
- .build();
+ @Override
+ protected AccountState fromDocument(JsonObject json, Set<String> fields) {
+ JsonElement source = json.get("_source");
+ if (source == null) {
+ source = json.getAsJsonObject().get("fields");
}
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<AccountState> read() throws OrmException {
- try {
- List<AccountState> results = Collections.emptyList();
- JestResult result = client.execute(search);
- if (result.isSucceeded()) {
- JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
- if (obj.get("hits") != null) {
- JsonArray json = obj.getAsJsonArray("hits");
- results = Lists.newArrayListWithCapacity(json.size());
- for (int i = 0; i < json.size(); i++) {
- results.add(toAccountState(json.get(i)));
- }
- }
- } else {
- log.error(result.getErrorMessage());
- }
- final List<AccountState> r = Collections.unmodifiableList(results);
- return new ResultSet<AccountState>() {
- @Override
- public Iterator<AccountState> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<AccountState> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- }
- }
-
- @Override
- public String toString() {
- return search.toString();
- }
-
- private AccountState toAccountState(JsonElement json) {
- JsonElement source = json.getAsJsonObject().get("_source");
- if (source == null) {
- source = json.getAsJsonObject().get("fields");
- }
-
- Account.Id id = new Account.Id(source.getAsJsonObject().get(ID.getName()).getAsInt());
- // Use the AccountCache rather than depending on any stored fields in the
- // document (of which there shouldn't be any). The most expensive part to
- // compute anyway is the effective group IDs, and we don't have a good way
- // to reindex when those change.
- return accountCache.get().get(id);
- }
+ Account.Id id = new Account.Id(source.getAsJsonObject().get(ID.getName()).getAsInt());
+ // Use the AccountCache rather than depending on any stored fields in the
+ // document (of which there shouldn't be any). The most expensive part to
+ // compute anyway is the effective group IDs, and we don't have a good way
+ // to reindex when those change.
+ return accountCache.get().get(id);
}
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 0fd2a77..b21d3df 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -35,6 +35,7 @@
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
@@ -53,38 +54,28 @@
import com.google.gerrit.server.index.change.ChangeIndexRewriter;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeDataSource;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.Search;
import io.searchbox.core.search.sort.Sort;
import io.searchbox.core.search.sort.Sort.Sorting;
import java.io.IOException;
import java.util.Collections;
-import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.lib.Config;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.builder.SearchSourceBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/** Secondary index implementation using Elasticsearch. */
class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
implements ChangeIndex {
- private static final Logger log = LoggerFactory.getLogger(ElasticChangeIndex.class);
-
static class ChangeMapping {
MappingProperties openChanges;
MappingProperties closedChanges;
@@ -153,7 +144,7 @@
}
@Override
- public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+ public DataSource<ChangeData> getSource(Predicate<ChangeData> p, QueryOptions opts)
throws QueryParseException {
Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
List<String> indexes = Lists.newArrayListWithCapacity(2);
@@ -163,7 +154,16 @@
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(CLOSED_CHANGES);
}
- return new QuerySource(indexes, p, opts);
+
+ List<Sort> sorts =
+ ImmutableList.of(
+ new Sort(ChangeField.UPDATED.getName(), Sorting.DESC),
+ new Sort(ChangeField.LEGACY_ID.getName(), Sorting.DESC));
+ for (Sort sort : sorts) {
+ sort.setIgnoreUnmapped();
+ }
+ QueryOptions filteredOpts = opts.filterFields(IndexUtils::changeFields);
+ return new ElasticQuerySource(p, filteredOpts, indexes, sorts);
}
@Override
@@ -181,297 +181,210 @@
return cd.getId().toString();
}
- private class QuerySource implements ChangeDataSource {
- private final Search search;
- private final Set<String> fields;
+ @Override
+ protected ChangeData fromDocument(JsonObject json, Set<String> fields) {
+ JsonElement sourceElement = json.get("_source");
+ if (sourceElement == null) {
+ sourceElement = json.getAsJsonObject().get("fields");
+ }
+ JsonObject source = sourceElement.getAsJsonObject();
+ JsonElement c = source.get(ChangeField.CHANGE.getName());
- QuerySource(List<String> types, Predicate<ChangeData> p, QueryOptions opts)
- throws QueryParseException {
- List<Sort> sorts =
- ImmutableList.of(
- new Sort(ChangeField.UPDATED.getName(), Sorting.DESC),
- new Sort(ChangeField.LEGACY_ID.getName(), Sorting.DESC));
- for (Sort sort : sorts) {
- sort.setIgnoreUnmapped();
+ if (c == null) {
+ int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
+ // IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
+ String projectName = checkNotNull(source.get(ChangeField.PROJECT.getName()).getAsString());
+ return changeDataFactory.create(
+ db.get(), new Project.NameKey(projectName), new Change.Id(id));
+ }
+
+ ChangeData cd =
+ changeDataFactory.create(
+ db.get(), CHANGE_CODEC.decode(Base64.decodeBase64(c.getAsString())));
+
+ // Any decoding that is done here must also be done in {@link LuceneChangeIndex}.
+
+ // Patch sets.
+ cd.setPatchSets(decodeProtos(source, ChangeField.PATCH_SET.getName(), PATCH_SET_CODEC));
+
+ // Approvals.
+ if (source.get(ChangeField.APPROVAL.getName()) != null) {
+ cd.setCurrentApprovals(decodeProtos(source, ChangeField.APPROVAL.getName(), APPROVAL_CODEC));
+ } else if (fields.contains(ChangeField.APPROVAL.getName())) {
+ cd.setCurrentApprovals(Collections.emptyList());
+ }
+
+ // Added & Deleted.
+ JsonElement addedElement = source.get(ChangeField.ADDED.getName());
+ JsonElement deletedElement = source.get(ChangeField.DELETED.getName());
+ if (addedElement != null && deletedElement != null) {
+ // Changed lines.
+ int added = addedElement.getAsInt();
+ int deleted = deletedElement.getAsInt();
+ if (added != 0 && deleted != 0) {
+ cd.setChangedLines(added, deleted);
}
- QueryBuilder qb = queryBuilder.toQueryBuilder(p);
- fields = IndexUtils.changeFields(opts);
- SearchSourceBuilder searchSource =
- new SearchSourceBuilder()
- .query(qb)
- .from(opts.start())
- .size(opts.limit())
- .fields(Lists.newArrayList(fields));
-
- search =
- new Search.Builder(searchSource.toString())
- .addType(types)
- .addSort(sorts)
- .addIndex(indexName)
- .build();
}
- @Override
- public int getCardinality() {
- return 10;
+ // Mergeable.
+ JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName());
+ if (mergeableElement != null) {
+ String mergeable = mergeableElement.getAsString();
+ if ("1".equals(mergeable)) {
+ cd.setMergeable(true);
+ } else if ("0".equals(mergeable)) {
+ cd.setMergeable(false);
+ }
}
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- try {
- List<ChangeData> results = Collections.emptyList();
- JestResult result = client.execute(search);
- if (result.isSucceeded()) {
- JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
- if (obj.get("hits") != null) {
- JsonArray json = obj.getAsJsonArray("hits");
- results = Lists.newArrayListWithCapacity(json.size());
- for (int i = 0; i < json.size(); i++) {
- results.add(toChangeData(json.get(i)));
- }
+ // Reviewed-by.
+ if (source.get(ChangeField.REVIEWEDBY.getName()) != null) {
+ JsonArray reviewedBy = source.get(ChangeField.REVIEWEDBY.getName()).getAsJsonArray();
+ if (reviewedBy.size() > 0) {
+ Set<Account.Id> accounts = Sets.newHashSetWithExpectedSize(reviewedBy.size());
+ for (int i = 0; i < reviewedBy.size(); i++) {
+ int aId = reviewedBy.get(i).getAsInt();
+ if (reviewedBy.size() == 1 && aId == ChangeField.NOT_REVIEWED) {
+ break;
}
- } else {
- log.error(result.getErrorMessage());
+ accounts.add(new Account.Id(aId));
}
- final List<ChangeData> r = Collections.unmodifiableList(results);
- return new ResultSet<ChangeData>() {
- @Override
- public Iterator<ChangeData> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<ChangeData> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
+ cd.setReviewedBy(accounts);
}
+ } else if (fields.contains(ChangeField.REVIEWEDBY.getName())) {
+ cd.setReviewedBy(Collections.emptySet());
}
- @Override
- public boolean hasChange() {
- return false;
- }
-
- @Override
- public String toString() {
- return search.toString();
- }
-
- private ChangeData toChangeData(JsonElement json) {
- JsonElement sourceElement = json.getAsJsonObject().get("_source");
- if (sourceElement == null) {
- sourceElement = json.getAsJsonObject().get("fields");
- }
- JsonObject source = sourceElement.getAsJsonObject();
- JsonElement c = source.get(ChangeField.CHANGE.getName());
-
- if (c == null) {
- int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
- // IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
- String projectName = checkNotNull(source.get(ChangeField.PROJECT.getName()).getAsString());
- return changeDataFactory.create(
- db.get(), new Project.NameKey(projectName), new Change.Id(id));
- }
-
- ChangeData cd =
- changeDataFactory.create(
- db.get(), CHANGE_CODEC.decode(Base64.decodeBase64(c.getAsString())));
-
- // Any decoding that is done here must also be done in {@link LuceneChangeIndex}.
-
- // Patch sets.
- cd.setPatchSets(decodeProtos(source, ChangeField.PATCH_SET.getName(), PATCH_SET_CODEC));
-
- // Approvals.
- if (source.get(ChangeField.APPROVAL.getName()) != null) {
- cd.setCurrentApprovals(
- decodeProtos(source, ChangeField.APPROVAL.getName(), APPROVAL_CODEC));
- } else if (fields.contains(ChangeField.APPROVAL.getName())) {
- cd.setCurrentApprovals(Collections.emptyList());
- }
-
- // Added & Deleted.
- JsonElement addedElement = source.get(ChangeField.ADDED.getName());
- JsonElement deletedElement = source.get(ChangeField.DELETED.getName());
- if (addedElement != null && deletedElement != null) {
- // Changed lines.
- int added = addedElement.getAsInt();
- int deleted = deletedElement.getAsInt();
- if (added != 0 && deleted != 0) {
- cd.setChangedLines(added, deleted);
+ // Hashtag.
+ if (source.get(ChangeField.HASHTAG.getName()) != null) {
+ JsonArray hashtagArray = source.get(ChangeField.HASHTAG.getName()).getAsJsonArray();
+ if (hashtagArray.size() > 0) {
+ Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtagArray.size());
+ for (int i = 0; i < hashtagArray.size(); i++) {
+ hashtags.add(hashtagArray.get(i).getAsString());
}
+ cd.setHashtags(hashtags);
}
-
- // Mergeable.
- JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName());
- if (mergeableElement != null) {
- String mergeable = mergeableElement.getAsString();
- if ("1".equals(mergeable)) {
- cd.setMergeable(true);
- } else if ("0".equals(mergeable)) {
- cd.setMergeable(false);
- }
- }
-
- // Reviewed-by.
- if (source.get(ChangeField.REVIEWEDBY.getName()) != null) {
- JsonArray reviewedBy = source.get(ChangeField.REVIEWEDBY.getName()).getAsJsonArray();
- if (reviewedBy.size() > 0) {
- Set<Account.Id> accounts = Sets.newHashSetWithExpectedSize(reviewedBy.size());
- for (int i = 0; i < reviewedBy.size(); i++) {
- int aId = reviewedBy.get(i).getAsInt();
- if (reviewedBy.size() == 1 && aId == ChangeField.NOT_REVIEWED) {
- break;
- }
- accounts.add(new Account.Id(aId));
- }
- cd.setReviewedBy(accounts);
- }
- } else if (fields.contains(ChangeField.REVIEWEDBY.getName())) {
- cd.setReviewedBy(Collections.emptySet());
- }
-
- // Hashtag.
- if (source.get(ChangeField.HASHTAG.getName()) != null) {
- JsonArray hashtagArray = source.get(ChangeField.HASHTAG.getName()).getAsJsonArray();
- if (hashtagArray.size() > 0) {
- Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtagArray.size());
- for (int i = 0; i < hashtagArray.size(); i++) {
- hashtags.add(hashtagArray.get(i).getAsString());
- }
- cd.setHashtags(hashtags);
- }
- } else if (fields.contains(ChangeField.HASHTAG.getName())) {
- cd.setHashtags(Collections.emptySet());
- }
-
- // Star.
- if (source.get(ChangeField.STAR.getName()) != null) {
- JsonArray starArray = source.get(ChangeField.STAR.getName()).getAsJsonArray();
- if (starArray.size() > 0) {
- ListMultimap<Account.Id, String> stars =
- MultimapBuilder.hashKeys().arrayListValues().build();
- for (int i = 0; i < starArray.size(); i++) {
- StarredChangesUtil.StarField starField =
- StarredChangesUtil.StarField.parse(starArray.get(i).getAsString());
- stars.put(starField.accountId(), starField.label());
- }
- cd.setStars(stars);
- }
- } else if (fields.contains(ChangeField.STAR.getName())) {
- cd.setStars(ImmutableListMultimap.of());
- }
-
- // Reviewer.
- if (source.get(ChangeField.REVIEWER.getName()) != null) {
- cd.setReviewers(
- ChangeField.parseReviewerFieldValues(
- FluentIterable.from(source.get(ChangeField.REVIEWER.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.REVIEWER.getName())) {
- cd.setReviewers(ReviewerSet.empty());
- }
-
- // Reviewer-by-email.
- if (source.get(ChangeField.REVIEWER_BY_EMAIL.getName()) != null) {
- cd.setReviewersByEmail(
- ChangeField.parseReviewerByEmailFieldValues(
- FluentIterable.from(
- source.get(ChangeField.REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.REVIEWER_BY_EMAIL.getName())) {
- cd.setReviewersByEmail(ReviewerByEmailSet.empty());
- }
-
- // Pending-reviewer.
- if (source.get(ChangeField.PENDING_REVIEWER.getName()) != null) {
- cd.setPendingReviewers(
- ChangeField.parseReviewerFieldValues(
- FluentIterable.from(
- source.get(ChangeField.PENDING_REVIEWER.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.PENDING_REVIEWER.getName())) {
- cd.setPendingReviewers(ReviewerSet.empty());
- }
-
- // Pending-reviewer-by-email.
- if (source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()) != null) {
- cd.setPendingReviewersByEmail(
- ChangeField.parseReviewerByEmailFieldValues(
- FluentIterable.from(
- source
- .get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName())
- .getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName())) {
- cd.setPendingReviewersByEmail(ReviewerByEmailSet.empty());
- }
-
- // Stored-submit-record-strict.
- decodeSubmitRecords(
- source,
- ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
- ChangeField.SUBMIT_RULE_OPTIONS_STRICT,
- cd);
-
- // Stored-submit-record-leniant.
- decodeSubmitRecords(
- source,
- ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
- ChangeField.SUBMIT_RULE_OPTIONS_LENIENT,
- cd);
-
- // Ref-state.
- if (fields.contains(ChangeField.REF_STATE.getName())) {
- cd.setRefStates(getByteArray(source, ChangeField.REF_STATE.getName()));
- }
-
- // Ref-state-pattern.
- if (fields.contains(ChangeField.REF_STATE_PATTERN.getName())) {
- cd.setRefStatePatterns(getByteArray(source, ChangeField.REF_STATE_PATTERN.getName()));
- }
-
- // Unresolved-comment-count.
- decodeUnresolvedCommentCount(source, ChangeField.UNRESOLVED_COMMENT_COUNT.getName(), cd);
-
- return cd;
+ } else if (fields.contains(ChangeField.HASHTAG.getName())) {
+ cd.setHashtags(Collections.emptySet());
}
- private Iterable<byte[]> getByteArray(JsonObject source, String name) {
- JsonElement element = source.get(name);
- return element != null
- ? Iterables.transform(element.getAsJsonArray(), e -> Base64.decodeBase64(e.getAsString()))
- : Collections.emptyList();
+ // Star.
+ if (source.get(ChangeField.STAR.getName()) != null) {
+ JsonArray starArray = source.get(ChangeField.STAR.getName()).getAsJsonArray();
+ if (starArray.size() > 0) {
+ ListMultimap<Account.Id, String> stars =
+ MultimapBuilder.hashKeys().arrayListValues().build();
+ for (int i = 0; i < starArray.size(); i++) {
+ StarredChangesUtil.StarField starField =
+ StarredChangesUtil.StarField.parse(starArray.get(i).getAsString());
+ stars.put(starField.accountId(), starField.label());
+ }
+ cd.setStars(stars);
+ }
+ } else if (fields.contains(ChangeField.STAR.getName())) {
+ cd.setStars(ImmutableListMultimap.of());
}
- private void decodeSubmitRecords(
- JsonObject doc, String fieldName, SubmitRuleOptions opts, ChangeData out) {
- JsonArray records = doc.getAsJsonArray(fieldName);
- if (records == null) {
- return;
- }
- ChangeField.parseSubmitRecords(
- FluentIterable.from(records)
- .transform(i -> new String(decodeBase64(i.toString()), UTF_8))
- .toList(),
- opts,
- out);
+ // Reviewer.
+ if (source.get(ChangeField.REVIEWER.getName()) != null) {
+ cd.setReviewers(
+ ChangeField.parseReviewerFieldValues(
+ FluentIterable.from(source.get(ChangeField.REVIEWER.getName()).getAsJsonArray())
+ .transform(JsonElement::getAsString)));
+ } else if (fields.contains(ChangeField.REVIEWER.getName())) {
+ cd.setReviewers(ReviewerSet.empty());
}
- private void decodeUnresolvedCommentCount(JsonObject doc, String fieldName, ChangeData out) {
- JsonElement count = doc.get(fieldName);
- if (count == null) {
- return;
- }
- out.setUnresolvedCommentCount(count.getAsInt());
+ // Reviewer-by-email.
+ if (source.get(ChangeField.REVIEWER_BY_EMAIL.getName()) != null) {
+ cd.setReviewersByEmail(
+ ChangeField.parseReviewerByEmailFieldValues(
+ FluentIterable.from(
+ source.get(ChangeField.REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
+ .transform(JsonElement::getAsString)));
+ } else if (fields.contains(ChangeField.REVIEWER_BY_EMAIL.getName())) {
+ cd.setReviewersByEmail(ReviewerByEmailSet.empty());
}
+
+ // Pending-reviewer.
+ if (source.get(ChangeField.PENDING_REVIEWER.getName()) != null) {
+ cd.setPendingReviewers(
+ ChangeField.parseReviewerFieldValues(
+ FluentIterable.from(
+ source.get(ChangeField.PENDING_REVIEWER.getName()).getAsJsonArray())
+ .transform(JsonElement::getAsString)));
+ } else if (fields.contains(ChangeField.PENDING_REVIEWER.getName())) {
+ cd.setPendingReviewers(ReviewerSet.empty());
+ }
+
+ // Pending-reviewer-by-email.
+ if (source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()) != null) {
+ cd.setPendingReviewersByEmail(
+ ChangeField.parseReviewerByEmailFieldValues(
+ FluentIterable.from(
+ source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
+ .transform(JsonElement::getAsString)));
+ } else if (fields.contains(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName())) {
+ cd.setPendingReviewersByEmail(ReviewerByEmailSet.empty());
+ }
+
+ // Stored-submit-record-strict.
+ decodeSubmitRecords(
+ source,
+ ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
+ ChangeField.SUBMIT_RULE_OPTIONS_STRICT,
+ cd);
+
+ // Stored-submit-record-leniant.
+ decodeSubmitRecords(
+ source,
+ ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
+ ChangeField.SUBMIT_RULE_OPTIONS_LENIENT,
+ cd);
+
+ // Ref-state.
+ if (fields.contains(ChangeField.REF_STATE.getName())) {
+ cd.setRefStates(getByteArray(source, ChangeField.REF_STATE.getName()));
+ }
+
+ // Ref-state-pattern.
+ if (fields.contains(ChangeField.REF_STATE_PATTERN.getName())) {
+ cd.setRefStatePatterns(getByteArray(source, ChangeField.REF_STATE_PATTERN.getName()));
+ }
+
+ // Unresolved-comment-count.
+ decodeUnresolvedCommentCount(source, ChangeField.UNRESOLVED_COMMENT_COUNT.getName(), cd);
+
+ return cd;
+ }
+
+ private Iterable<byte[]> getByteArray(JsonObject source, String name) {
+ JsonElement element = source.get(name);
+ return element != null
+ ? Iterables.transform(element.getAsJsonArray(), e -> Base64.decodeBase64(e.getAsString()))
+ : Collections.emptyList();
+ }
+
+ private void decodeSubmitRecords(
+ JsonObject doc, String fieldName, SubmitRuleOptions opts, ChangeData out) {
+ JsonArray records = doc.getAsJsonArray(fieldName);
+ if (records == null) {
+ return;
+ }
+ ChangeField.parseSubmitRecords(
+ FluentIterable.from(records)
+ .transform(i -> new String(decodeBase64(i.toString()), UTF_8))
+ .toList(),
+ opts,
+ out);
+ }
+
+ private void decodeUnresolvedCommentCount(JsonObject doc, String fieldName, ChangeData out) {
+ JsonElement count = doc.get(fieldName);
+ if (count == null) {
+ return;
+ }
+ out.setUnresolvedCommentCount(count.getAsInt());
}
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index 6ca4ad5..7f0ec34 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -14,9 +14,7 @@
package com.google.gerrit.elasticsearch;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
@@ -31,31 +29,18 @@
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.group.GroupField;
import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.Search;
import io.searchbox.core.search.sort.Sort;
-import io.searchbox.core.search.sort.Sort.Sorting;
import java.io.IOException;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.builder.SearchSourceBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class ElasticGroupIndex extends AbstractElasticIndex<AccountGroup.UUID, InternalGroup>
implements GroupIndex {
@@ -70,8 +55,6 @@
static final String GROUPS = "groups";
static final String GROUPS_PREFIX = GROUPS + "_";
- private static final Logger log = LoggerFactory.getLogger(ElasticGroupIndex.class);
-
private final GroupMapping mapping;
private final Provider<GroupCache> groupCache;
@@ -108,7 +91,9 @@
@Override
public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(p, opts);
+ Sort sort = new Sort(GroupField.UUID.getName(), Sort.Sorting.ASC);
+ sort.setIgnoreUnmapped();
+ return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), GROUPS, sort);
}
@Override
@@ -127,93 +112,18 @@
return group.getGroupUUID().get();
}
- private class QuerySource implements DataSource<InternalGroup> {
- private final Search search;
- private final Set<String> fields;
-
- QuerySource(Predicate<InternalGroup> p, QueryOptions opts) throws QueryParseException {
- QueryBuilder qb = queryBuilder.toQueryBuilder(p);
- fields = IndexUtils.groupFields(opts);
- SearchSourceBuilder searchSource =
- new SearchSourceBuilder()
- .query(qb)
- .from(opts.start())
- .size(opts.limit())
- .fields(Lists.newArrayList(fields));
-
- Sort sort = new Sort(GroupField.UUID.getName(), Sorting.ASC);
- sort.setIgnoreUnmapped();
-
- search =
- new Search.Builder(searchSource.toString())
- .addType(GROUPS)
- .addIndex(indexName)
- .addSort(ImmutableList.of(sort))
- .build();
+ @Override
+ protected InternalGroup fromDocument(JsonObject json, Set<String> fields) {
+ JsonElement source = json.get("_source");
+ if (source == null) {
+ source = json.getAsJsonObject().get("fields");
}
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<InternalGroup> read() throws OrmException {
- try {
- List<InternalGroup> results = Collections.emptyList();
- JestResult result = client.execute(search);
- if (result.isSucceeded()) {
- JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
- if (obj.get("hits") != null) {
- JsonArray json = obj.getAsJsonArray("hits");
- results = Lists.newArrayListWithCapacity(json.size());
- for (int i = 0; i < json.size(); i++) {
- Optional<InternalGroup> internalGroup = toInternalGroup(json.get(i));
- internalGroup.ifPresent(results::add);
- }
- }
- } else {
- log.error(result.getErrorMessage());
- }
- final List<InternalGroup> r = Collections.unmodifiableList(results);
- return new ResultSet<InternalGroup>() {
- @Override
- public Iterator<InternalGroup> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<InternalGroup> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- }
- }
-
- @Override
- public String toString() {
- return search.toString();
- }
-
- private Optional<InternalGroup> toInternalGroup(JsonElement json) {
- JsonElement source = json.getAsJsonObject().get("_source");
- if (source == null) {
- source = json.getAsJsonObject().get("fields");
- }
-
- AccountGroup.UUID uuid =
- new AccountGroup.UUID(
- source.getAsJsonObject().get(GroupField.UUID.getName()).getAsString());
- // Use the GroupCache rather than depending on any stored fields in the
- // document (of which there shouldn't be any).
- return groupCache.get().get(uuid);
- }
+ AccountGroup.UUID uuid =
+ new AccountGroup.UUID(
+ source.getAsJsonObject().get(GroupField.UUID.getName()).getAsString());
+ // Use the GroupCache rather than depending on any stored fields in the
+ // document (of which there shouldn't be any).
+ return groupCache.get().get(uuid).orElse(null);
}
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index 780f023..a564e5b 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -14,9 +14,7 @@
package com.google.gerrit.elasticsearch;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
@@ -31,30 +29,19 @@
import com.google.gerrit.server.index.project.ProjectIndex;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectData;
-import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.Search;
import io.searchbox.core.search.sort.Sort;
import io.searchbox.core.search.sort.Sort.Sorting;
import java.io.IOException;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.builder.SearchSourceBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class ElasticProjectIndex extends AbstractElasticIndex<Project.NameKey, ProjectData>
implements ProjectIndex {
@@ -69,8 +56,6 @@
static final String PROJECTS = "projects";
static final String PROJECTS_PREFIX = PROJECTS + "_";
- private static final Logger log = LoggerFactory.getLogger(ElasticProjectIndex.class);
-
private final ProjectMapping mapping;
private final Provider<ProjectCache> projectCache;
@@ -107,7 +92,9 @@
@Override
public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(p, opts);
+ Sort sort = new Sort(ProjectField.NAME.getName(), Sorting.ASC);
+ sort.setIgnoreUnmapped();
+ return new ElasticQuerySource(p, opts.filterFields(IndexUtils::projectFields), PROJECTS, sort);
}
@Override
@@ -126,90 +113,16 @@
return projectState.getProject().getName();
}
- private class QuerySource implements DataSource<ProjectData> {
- private final Search search;
- private final Set<String> fields;
-
- QuerySource(Predicate<ProjectData> p, QueryOptions opts) throws QueryParseException {
- QueryBuilder qb = queryBuilder.toQueryBuilder(p);
- fields = IndexUtils.projectFields(opts);
- SearchSourceBuilder searchSource =
- new SearchSourceBuilder()
- .query(qb)
- .from(opts.start())
- .size(opts.limit())
- .fields(Lists.newArrayList(fields));
-
- Sort sort = new Sort(ProjectField.NAME.getName(), Sorting.ASC);
- sort.setIgnoreUnmapped();
-
- search =
- new Search.Builder(searchSource.toString())
- .addType(PROJECTS)
- .addIndex(indexName)
- .addSort(ImmutableList.of(sort))
- .build();
+ @Override
+ protected ProjectData fromDocument(JsonObject json, Set<String> fields) {
+ JsonElement source = json.get("_source");
+ if (source == null) {
+ source = json.getAsJsonObject().get("fields");
}
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<ProjectData> read() throws OrmException {
- try {
- List<ProjectData> results = Collections.emptyList();
- JestResult result = client.execute(search);
- if (result.isSucceeded()) {
- JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
- if (obj.get("hits") != null) {
- JsonArray json = obj.getAsJsonArray("hits");
- results = Lists.newArrayListWithCapacity(json.size());
- for (int i = 0; i < json.size(); i++) {
- results.add(toProjectData(json.get(i)));
- }
- }
- } else {
- log.error(result.getErrorMessage());
- }
- final List<ProjectData> r = Collections.unmodifiableList(results);
- return new ResultSet<ProjectData>() {
- @Override
- public Iterator<ProjectData> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<ProjectData> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- }
- }
-
- @Override
- public String toString() {
- return search.toString();
- }
-
- private ProjectData toProjectData(JsonElement json) {
- JsonElement source = json.getAsJsonObject().get("_source");
- if (source == null) {
- source = json.getAsJsonObject().get("fields");
- }
-
- Project.NameKey nameKey =
- new Project.NameKey(
- source.getAsJsonObject().get(ProjectField.NAME.getName()).getAsString());
- return projectCache.get().get(nameKey).toProjectData();
- }
+ Project.NameKey nameKey =
+ new Project.NameKey(
+ source.getAsJsonObject().get(ProjectField.NAME.getName()).getAsString());
+ return projectCache.get().get(nameKey).toProjectData();
}
}
diff --git a/java/com/google/gerrit/extensions/api/projects/Projects.java b/java/com/google/gerrit/extensions/api/projects/Projects.java
index 02cce3a..b33f61e 100644
--- a/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -92,6 +92,7 @@
private int limit;
private int start;
private boolean showTree;
+ private boolean all;
private FilterType type = FilterType.ALL;
public List<ProjectInfo> get() throws RestApiException {
@@ -152,6 +153,11 @@
return this;
}
+ public ListRequest withAll() {
+ this.all = true;
+ return this;
+ }
+
public boolean getDescription() {
return description;
}
@@ -187,6 +193,10 @@
public FilterType getFilterType() {
return type;
}
+
+ public boolean isAll() {
+ return all;
+ }
}
/**
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 160c14a..54d44c6 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -8,5 +8,6 @@
"//java/com/google/gerrit/extensions/client/testing:client-test-util",
"//java/com/google/gerrit/truth",
"//lib:truth",
+ "//lib/jgit/org.eclipse.jgit:jgit",
],
)
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index 7495a52..cdbef34 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -24,6 +24,8 @@
import com.google.common.truth.Truth;
import com.google.gerrit.extensions.common.GitPerson;
import java.sql.Timestamp;
+import java.util.Date;
+import org.eclipse.jgit.lib.PersonIdent;
public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> {
@@ -65,4 +67,14 @@
date().isEqualTo(other.date);
tz().isEqualTo(other.tz);
}
+
+ public void matches(PersonIdent ident) {
+ isNotNull();
+ name().isEqualTo(ident.getName());
+ email().isEqualTo(ident.getEmailAddress());
+ Truth.assertThat(new Date(actual().date.getTime()))
+ .named("rounded date")
+ .isEqualTo(ident.getWhen());
+ tz().isEqualTo(ident.getTimeZoneOffset());
+ }
}
diff --git a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 0df0aa9..967259a 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -27,7 +27,7 @@
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/java/com/google/gerrit/gpg/api/GpgApiModule.java b/java/com/google/gerrit/gpg/api/GpgApiModule.java
index f7102d8..f0d34f3 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiModule.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiModule.java
@@ -29,7 +29,7 @@
import com.google.gerrit.gpg.server.PostGpgKeys;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.account.GpgApiAdapter;
import java.util.List;
import java.util.Map;
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 8e1f898..7fc3074 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -17,6 +17,7 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
+ "//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/sshd",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 44203dc..b5995a8 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.StartupChecks;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
+import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeCleanupRunner;
import com.google.gerrit.server.config.AuthConfig;
@@ -70,6 +71,7 @@
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.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
@@ -306,6 +308,7 @@
}
modules.add(new DatabaseModule());
modules.add(new NotesMigration.Module());
+ modules.add(new GroupsMigration.Module());
modules.add(new DropWizardMetricMaker.ApiModule());
return Guice.createInjector(PRODUCTION, modules);
}
@@ -330,6 +333,7 @@
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+ modules.add(new GerritApiModule());
modules.add(new SearchingChangeCacheImpl.Module());
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultPermissionBackendModule());
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index 34f7d33..2d7e31e 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -15,6 +15,7 @@
package com.google.gerrit.index;
import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
@@ -102,14 +103,35 @@
} catch (OrmException e) {
throw new IOException(e);
}
- switch (results.size()) {
- case 0:
- return Optional.empty();
- case 1:
- return Optional.of(results.get(0));
- default:
- throw new IOException("Multiple results found in index for key " + key + ": " + results);
+ if (results.size() > 1) {
+ throw new IOException("Multiple results found in index for key " + key + ": " + results);
}
+ return results.stream().findFirst();
+ }
+
+ /**
+ * Get a single raw document from the index.
+ *
+ * @param key document key.
+ * @param opts query options. Options that do not make sense in the context of a single document,
+ * such as start, will be ignored.
+ * @return an abstraction of a raw index document to retrieve fields from.
+ * @throws IOException
+ */
+ default Optional<FieldBundle> getRaw(K key, QueryOptions opts) throws IOException {
+ opts = opts.withStart(0).withLimit(2);
+ List<FieldBundle> results;
+ try {
+ results = getSource(keyPredicate(key), opts).readRaw().toList();
+ } catch (QueryParseException e) {
+ throw new IOException("Unexpected QueryParseException during get()", e);
+ } catch (OrmException e) {
+ throw new IOException(e);
+ }
+ if (results.size() > 1) {
+ throw new IOException("Multiple results found in index for key " + key + ": " + results);
+ }
+ return results.stream().findFirst();
}
/**
diff --git a/java/com/google/gerrit/index/IndexedQuery.java b/java/com/google/gerrit/index/IndexedQuery.java
index 050b4a9..143cc26 100644
--- a/java/com/google/gerrit/index/IndexedQuery.java
+++ b/java/com/google/gerrit/index/IndexedQuery.java
@@ -17,6 +17,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Paginated;
import com.google.gerrit.index.query.Predicate;
@@ -86,6 +87,11 @@
}
@Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ return source.readRaw();
+ }
+
+ @Override
public ResultSet<T> restart(int start) throws OrmException {
opts = opts.withStart(start);
try {
diff --git a/java/com/google/gerrit/index/QueryOptions.java b/java/com/google/gerrit/index/QueryOptions.java
index b57fb5f..0401dab 100644
--- a/java/com/google/gerrit/index/QueryOptions.java
+++ b/java/com/google/gerrit/index/QueryOptions.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import java.util.Set;
+import java.util.function.Function;
@AutoValue
public abstract class QueryOptions {
@@ -53,4 +54,8 @@
public QueryOptions withStart(int newStart) {
return create(config(), newStart, limit(), fields());
}
+
+ public QueryOptions filterFields(Function<QueryOptions, Set<String>> filter) {
+ return create(config(), start(), limit(), filter.apply(this));
+ }
}
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 16620b3..e2605f4 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -89,6 +89,12 @@
}
}
+ @Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ // TOOD(hiesel): Implement
+ throw new UnsupportedOperationException("not implemented");
+ }
+
private ResultSet<T> readImpl() throws OrmException {
if (source == null) {
throw new OrmException("No DataSource: " + this);
diff --git a/java/com/google/gerrit/index/query/DataSource.java b/java/com/google/gerrit/index/query/DataSource.java
index 77dcca2..88cc0e3c 100644
--- a/java/com/google/gerrit/index/query/DataSource.java
+++ b/java/com/google/gerrit/index/query/DataSource.java
@@ -23,4 +23,7 @@
/** @return read from the database and return the results. */
ResultSet<T> read() throws OrmException;
+
+ /** @return read from the database and return the raw results. */
+ ResultSet<FieldBundle> readRaw() throws OrmException;
}
diff --git a/java/com/google/gerrit/index/query/FieldBundle.java b/java/com/google/gerrit/index/query/FieldBundle.java
new file mode 100644
index 0000000..6ecb6e6
--- /dev/null
+++ b/java/com/google/gerrit/index/query/FieldBundle.java
@@ -0,0 +1,61 @@
+// 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.index.query;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.index.FieldDef;
+
+/** FieldBundle is an abstraction that allows retrieval of raw values from different sources. */
+public class FieldBundle {
+
+ // Map String => List{Integer, Long, Timestamp, String, byte[]}
+ private ImmutableListMultimap<String, Object> fields;
+
+ public FieldBundle(ListMultimap<String, Object> fields) {
+ this.fields = ImmutableListMultimap.copyOf(fields);
+ }
+
+ /**
+ * Get a field's value based on the field definition.
+ *
+ * @param fieldDef the definition of the field of which the value should be retrieved. The field
+ * must be stored and contained in the result set as specified by {@link
+ * com.google.gerrit.index.QueryOptions}.
+ * @param <T> Data type of the returned object based on the field definition
+ * @return Either a single element or an Iterable based on the field definition. An empty list is
+ * returned for repeated fields that are not contained in the result.
+ * @throws IllegalArgumentException if the requested field is not stored or not present. This
+ * check is only enforced on non-repeatable fields.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T getValue(FieldDef<?, T> fieldDef) {
+ checkArgument(fieldDef.isStored(), "Field must be stored");
+ checkArgument(
+ fields.containsKey(fieldDef.getName()) || fieldDef.isRepeatable(),
+ "Field %s is not in result set %s",
+ fieldDef.getName(),
+ fields.keySet());
+
+ Iterable<Object> result = fields.get(fieldDef.getName());
+ if (fieldDef.isRepeatable()) {
+ return (T) result;
+ }
+ return (T) Iterables.getOnlyElement(result);
+ }
+}
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 9d474dd..0c261f6 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -14,10 +14,13 @@
package com.google.gerrit.lucene;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.Futures;
@@ -28,12 +31,22 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.Schema.Values;
+import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -43,6 +56,7 @@
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
@@ -52,13 +66,18 @@
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TrackingIndexWriter;
import org.apache.lucene.search.ControlledRealTimeReopenThread;
import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
import org.apache.lucene.search.ReferenceManager;
import org.apache.lucene.search.ReferenceManager.RefreshListener;
+import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.slf4j.Logger;
@@ -294,6 +313,8 @@
return result;
}
+ protected abstract V fromDocument(Document doc);
+
void add(Document doc, Values<V> values) {
String name = values.getField().getName();
FieldType<?> type = values.getField().getType();
@@ -328,6 +349,29 @@
}
}
+ protected FieldBundle toFieldBundle(Document doc) {
+ Map<String, FieldDef<V, ?>> allFields = getSchema().getFields();
+ ListMultimap<String, Object> rawFields = ArrayListMultimap.create();
+ for (IndexableField field : doc.getFields()) {
+ checkArgument(allFields.containsKey(field.name()), "Unrecognized field " + field.name());
+ FieldType<?> type = allFields.get(field.name()).getType();
+ if (type == FieldType.EXACT || type == FieldType.FULL_TEXT || type == FieldType.PREFIX) {
+ rawFields.put(field.name(), field.stringValue());
+ } else if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
+ rawFields.put(field.name(), field.numericValue().intValue());
+ } else if (type == FieldType.LONG) {
+ rawFields.put(field.name(), field.numericValue().longValue());
+ } else if (type == FieldType.TIMESTAMP) {
+ rawFields.put(field.name(), new Timestamp(field.numericValue().longValue()));
+ } else if (type == FieldType.STORED_ONLY) {
+ rawFields.put(field.name(), field.binaryValue().bytes);
+ } else {
+ throw FieldType.badFieldType(type);
+ }
+ }
+ return new FieldBundle(rawFields);
+ }
+
private static Field.Store store(FieldDef<?, ?> f) {
return f.isStored() ? Field.Store.YES : Field.Store.NO;
}
@@ -416,4 +460,76 @@
public Schema<V> getSchema() {
return schema;
}
+
+ protected class LuceneQuerySource implements DataSource<V> {
+ private final QueryOptions opts;
+ private final Query query;
+ private final Sort sort;
+
+ LuceneQuerySource(QueryOptions opts, Query query, Sort sort) {
+ this.opts = opts;
+ this.query = query;
+ this.sort = sort;
+ }
+
+ @Override
+ public int getCardinality() {
+ return 10;
+ }
+
+ @Override
+ public ResultSet<V> read() throws OrmException {
+ return readImpl((doc) -> fromDocument(doc));
+ }
+
+ @Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ return readImpl(AbstractLuceneIndex.this::toFieldBundle);
+ }
+
+ private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
+ IndexSearcher searcher = null;
+ try {
+ searcher = acquire();
+ int realLimit = opts.start() + opts.limit();
+ TopFieldDocs docs = searcher.search(query, realLimit, sort);
+ List<T> result = new ArrayList<>(docs.scoreDocs.length);
+ for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
+ ScoreDoc sd = docs.scoreDocs[i];
+ Document doc = searcher.doc(sd.doc, opts.fields());
+ T mapperResult = mapper.apply(doc);
+ if (mapperResult != null) {
+ result.add(mapperResult);
+ }
+ }
+ final List<T> r = Collections.unmodifiableList(result);
+ return new ResultSet<T>() {
+ @Override
+ public Iterator<T> iterator() {
+ return r.iterator();
+ }
+
+ @Override
+ public List<T> toList() {
+ return r;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } finally {
+ if (searcher != null) {
+ try {
+ release(searcher);
+ } catch (IOException e) {
+ log.warn("cannot release Lucene searcher", e);
+ }
+ }
+ }
+ }
+ }
}
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 126c79f..7d7cbef 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -24,6 +24,7 @@
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.Schema.Values;
import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Change;
@@ -85,6 +86,12 @@
throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
}
+ // Make method public so that it can be used in LuceneChangeIndex
+ @Override
+ public FieldBundle toFieldBundle(Document doc) {
+ return super.toFieldBundle(doc);
+ }
+
@Override
void add(Document doc, Values<ChangeData> values) {
// Add separate DocValues fields for those fields needed for sorting.
@@ -98,4 +105,9 @@
}
super.add(doc, values);
}
+
+ @Override
+ protected ChangeData fromDocument(Document doc) {
+ throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
+ }
}
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 7a4cd40..8ceea0d 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -28,38 +28,24 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.account.AccountIndex;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.concurrent.ExecutionException;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class LuceneAccountIndex extends AbstractLuceneIndex<Account.Id, AccountState>
implements AccountIndex {
- private static final Logger log = LoggerFactory.getLogger(LuceneAccountIndex.class);
-
private static final String ACCOUNTS = "accounts";
private static final String ID_SORT_FIELD = sortFieldName(ID);
@@ -127,76 +113,14 @@
@Override
public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(
- opts,
+ return new LuceneQuerySource(
+ opts.filterFields(IndexUtils::accountFields),
queryBuilder.toQuery(p),
new Sort(new SortField(ID_SORT_FIELD, SortField.Type.LONG, true)));
}
- private class QuerySource implements DataSource<AccountState> {
- private final QueryOptions opts;
- private final Query query;
- private final Sort sort;
-
- private QuerySource(QueryOptions opts, Query query, Sort sort) {
- this.opts = opts;
- this.query = query;
- this.sort = sort;
- }
-
- @Override
- public int getCardinality() {
- // TODO(dborowitz): In contrast to the comment in
- // LuceneChangeIndex.QuerySource#getCardinality, at this point I actually
- // think we might just want to remove getCardinality.
- return 10;
- }
-
- @Override
- public ResultSet<AccountState> read() throws OrmException {
- IndexSearcher searcher = null;
- try {
- searcher = acquire();
- int realLimit = opts.start() + opts.limit();
- TopFieldDocs docs = searcher.search(query, realLimit, sort);
- List<AccountState> result = new ArrayList<>(docs.scoreDocs.length);
- for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
- ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searcher.doc(sd.doc, IndexUtils.accountFields(opts));
- result.add(toAccountState(doc));
- }
- final List<AccountState> r = Collections.unmodifiableList(result);
- return new ResultSet<AccountState>() {
- @Override
- public Iterator<AccountState> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<AccountState> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- } finally {
- if (searcher != null) {
- try {
- release(searcher);
- } catch (IOException e) {
- log.warn("cannot release Lucene searcher", e);
- }
- }
- }
- }
- }
-
- private AccountState toAccountState(Document doc) {
+ @Override
+ protected AccountState fromDocument(Document doc) {
Account.Id id = new Account.Id(doc.getField(ID.getName()).numericValue().intValue());
// Use the AccountCache rather than depending on any stored fields in the
// document (of which there shouldn't be any). The most expensive part to
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index b004333..b30e66c 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -26,6 +26,7 @@
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
import static java.util.stream.Collectors.toList;
+import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
@@ -37,6 +38,7 @@
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
@@ -243,7 +245,7 @@
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(closedIndex);
}
- return new QuerySource(indexes, p, opts, getSort());
+ return new QuerySource(indexes, p, opts, getSort(), openIndex::toFieldBundle);
}
@Override
@@ -269,15 +271,21 @@
private final Query query;
private final QueryOptions opts;
private final Sort sort;
+ private final Function<Document, FieldBundle> rawDocumentMapper;
private QuerySource(
- List<ChangeSubIndex> indexes, Predicate<ChangeData> predicate, QueryOptions opts, Sort sort)
+ List<ChangeSubIndex> indexes,
+ Predicate<ChangeData> predicate,
+ QueryOptions opts,
+ Sort sort,
+ Function<Document, FieldBundle> rawDocumentMapper)
throws QueryParseException {
this.indexes = indexes;
this.predicate = predicate;
this.query = checkNotNull(queryBuilder.toQuery(predicate), "null query from Lucene");
this.opts = opts;
this.sort = sort;
+ this.rawDocumentMapper = rawDocumentMapper;
}
@Override
@@ -319,6 +327,33 @@
fields);
}
+ @Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ List<Document> documents;
+ try {
+ documents = doRead(IndexUtils.changeFields(opts));
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ List<FieldBundle> fieldBundles = documents.stream().map(rawDocumentMapper).collect(toList());
+ return new ResultSet<FieldBundle>() {
+ @Override
+ public Iterator<FieldBundle> iterator() {
+ return fieldBundles.iterator();
+ }
+
+ @Override
+ public List<FieldBundle> toList() {
+ return fieldBundles;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ }
+
private List<Document> doRead(Set<String> fields) throws IOException {
IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
try {
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index 32870cb..7878afe 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -28,38 +28,24 @@
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class LuceneGroupIndex extends AbstractLuceneIndex<AccountGroup.UUID, InternalGroup>
implements GroupIndex {
- private static final Logger log = LoggerFactory.getLogger(LuceneGroupIndex.class);
private static final String GROUPS = "groups";
@@ -128,77 +114,17 @@
@Override
public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(
- opts,
+ return new LuceneQuerySource(
+ opts.filterFields(IndexUtils::groupFields),
queryBuilder.toQuery(p),
new Sort(new SortField(UUID_SORT_FIELD, SortField.Type.STRING, false)));
}
- private class QuerySource implements DataSource<InternalGroup> {
- private final QueryOptions opts;
- private final Query query;
- private final Sort sort;
-
- private QuerySource(QueryOptions opts, Query query, Sort sort) {
- this.opts = opts;
- this.query = query;
- this.sort = sort;
- }
-
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<InternalGroup> read() throws OrmException {
- IndexSearcher searcher = null;
- try {
- searcher = acquire();
- int realLimit = opts.start() + opts.limit();
- TopFieldDocs docs = searcher.search(query, realLimit, sort);
- List<InternalGroup> result = new ArrayList<>(docs.scoreDocs.length);
- for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
- ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searcher.doc(sd.doc, IndexUtils.groupFields(opts));
- Optional<InternalGroup> internalGroup = toInternalGroup(doc);
- internalGroup.ifPresent(result::add);
- }
- final List<InternalGroup> r = Collections.unmodifiableList(result);
- return new ResultSet<InternalGroup>() {
- @Override
- public Iterator<InternalGroup> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<InternalGroup> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- } finally {
- if (searcher != null) {
- try {
- release(searcher);
- } catch (IOException e) {
- log.warn("cannot release Lucene searcher", e);
- }
- }
- }
- }
- }
-
- private Optional<InternalGroup> toInternalGroup(Document doc) {
+ @Override
+ protected InternalGroup fromDocument(Document doc) {
AccountGroup.UUID uuid = new AccountGroup.UUID(doc.getField(UUID.getName()).stringValue());
// Use the GroupCache rather than depending on any stored fields in the
// document (of which there shouldn't be any).
- return groupCache.get().get(uuid);
+ return groupCache.get().get(uuid).orElse(null);
}
}
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 6354f61..e776a8b 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -28,38 +28,24 @@
import com.google.gerrit.server.index.project.ProjectIndex;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectData;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.concurrent.ExecutionException;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class LuceneProjectIndex extends AbstractLuceneIndex<Project.NameKey, ProjectData>
implements ProjectIndex {
- private static final Logger log = LoggerFactory.getLogger(LuceneProjectIndex.class);
-
private static final String PROJECTS = "projects";
private static final String NAME_SORT_FIELD = sortFieldName(NAME);
@@ -127,73 +113,14 @@
@Override
public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(
- opts,
+ return new LuceneQuerySource(
+ opts.filterFields(IndexUtils::projectFields),
queryBuilder.toQuery(p),
new Sort(new SortField(NAME_SORT_FIELD, SortField.Type.STRING, false)));
}
- private class QuerySource implements DataSource<ProjectData> {
- private final QueryOptions opts;
- private final Query query;
- private final Sort sort;
-
- private QuerySource(QueryOptions opts, Query query, Sort sort) {
- this.opts = opts;
- this.query = query;
- this.sort = sort;
- }
-
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<ProjectData> read() throws OrmException {
- IndexSearcher searcher = null;
- try {
- searcher = acquire();
- int realLimit = opts.start() + opts.limit();
- TopFieldDocs docs = searcher.search(query, realLimit, sort);
- List<ProjectData> result = new ArrayList<>(docs.scoreDocs.length);
- for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
- ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searcher.doc(sd.doc, IndexUtils.projectFields(opts));
- result.add(toProjectData(doc));
- }
- final List<ProjectData> r = Collections.unmodifiableList(result);
- return new ResultSet<ProjectData>() {
- @Override
- public Iterator<ProjectData> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<ProjectData> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- } finally {
- if (searcher != null) {
- try {
- release(searcher);
- } catch (IOException e) {
- log.warn("cannot release Lucene searcher", e);
- }
- }
- }
- }
- }
-
- private ProjectData toProjectData(Document doc) {
+ @Override
+ protected ProjectData fromDocument(Document doc) {
Project.NameKey nameKey = new Project.NameKey(doc.getField(NAME.getName()).stringValue());
return projectCache.get().get(nameKey).toProjectData();
}
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index dce3ea1..4dfaf1c 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -35,6 +35,7 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
+ "//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/sshd",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index a369953..d72b46f 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -52,6 +52,7 @@
import com.google.gerrit.server.StartupChecks;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
+import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeCleanupRunner;
import com.google.gerrit.server.config.AuthConfig;
@@ -411,6 +412,8 @@
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+ modules.add(new GerritApiModule());
+
modules.add(new SearchingChangeCacheImpl.Module(slave));
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultPermissionBackendModule());
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 80bdb4b..3385244 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -20,7 +20,9 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
import com.google.gerrit.pgm.init.api.InitFlags;
@@ -37,7 +39,10 @@
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.GroupConfig;
+import com.google.gerrit.server.group.db.GroupNameNotes;
+import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -46,6 +51,7 @@
import java.nio.file.Path;
import java.sql.Timestamp;
import java.util.List;
+import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.PersonIdent;
@@ -67,43 +73,91 @@
private final InitFlags flags;
private final SitePaths site;
private final String allUsers;
- private final boolean writeGroupsToNoteDb;
+ private final GroupsMigration groupsMigration;
@Inject
public GroupsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
this.flags = flags;
this.site = site;
this.allUsers = allUsers.get();
- // TODO(aliceks): Remove this flag when all other necessary TODOs for writing groups to NoteDb
- // have been addressed.
- // Don't flip this flag in a production setting! We only added it to spread the implementation
- // of groups in NoteDb among several changes which are gradually merged.
- writeGroupsToNoteDb = flags.cfg.getBoolean("user", null, "writeGroupsToNoteDb", false);
+ this.groupsMigration = new GroupsMigration(flags.cfg);
}
/**
- * Returns the {@code AccountGroup} for the specified name.
+ * Returns the {@code AccountGroup} for the specified {@code GroupReference}.
*
* @param db the {@code ReviewDb} instance to use for lookups
- * @param groupName the name of the group
- * @return the {@code AccountGroup} which has the specified name
+ * @param groupReference the {@code GroupReference} of the group
+ * @return the {@code InternalGroup} represented by the {@code GroupReference}
* @throws OrmException if the group couldn't be retrieved from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
* @throws NoSuchGroupException if a group with such a name doesn't exist
*/
- public AccountGroup getExistingGroup(ReviewDb db, AccountGroup.NameKey groupName)
- throws OrmException, NoSuchGroupException {
- // TODO(aliceks): Add implementation for NoteDb.
- AccountGroupName accountGroupName = db.accountGroupNames().get(groupName);
+ public InternalGroup getExistingGroup(ReviewDb db, GroupReference groupReference)
+ throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ return getExistingGroupFromNoteDb(groupReference);
+ }
+
+ return getExistingGroupFromReviewDb(db, groupReference);
+ }
+
+ private InternalGroup getExistingGroupFromNoteDb(GroupReference groupReference)
+ throws IOException, ConfigInvalidException, NoSuchGroupException {
+ File allUsersRepoPath = getPathToAllUsersRepository();
+ if (allUsersRepoPath != null) {
+ try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
+ AccountGroup.UUID groupUuid = groupReference.getUUID();
+ GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, groupUuid);
+ return groupConfig
+ .getLoadedGroup()
+ .orElseThrow(() -> new NoSuchGroupException(groupReference.getUUID()));
+ }
+ }
+ throw new NoSuchGroupException(groupReference.getUUID());
+ }
+
+ private static InternalGroup getExistingGroupFromReviewDb(
+ ReviewDb db, GroupReference groupReference) throws OrmException, NoSuchGroupException {
+ String groupName = groupReference.getName();
+ AccountGroupName accountGroupName =
+ db.accountGroupNames().get(new AccountGroup.NameKey(groupName));
if (accountGroupName == null) {
- throw new NoSuchGroupException(groupName.toString());
+ throw new NoSuchGroupException(groupName);
}
AccountGroup.Id groupId = accountGroupName.getId();
AccountGroup group = db.accountGroups().get(groupId);
if (group == null) {
- throw new NoSuchGroupException(groupName.toString());
+ throw new NoSuchGroupException(groupName);
}
- return group;
+ return Groups.asInternalGroup(db, group);
+ }
+
+ /**
+ * Returns {@code GroupReference}s for all internal groups.
+ *
+ * @param db the {@code ReviewDb} instance to use for lookups
+ * @return a stream of the {@code GroupReference}s of all internal groups
+ * @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
+ */
+ public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ File allUsersRepoPath = getPathToAllUsersRepository();
+ if (allUsersRepoPath != null) {
+ try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
+ return GroupNameNotes.loadAllGroupReferences(allUsersRepo).stream();
+ }
+ }
+ return Stream.empty();
+ }
+
+ return Streams.stream(db.accountGroups().all())
+ .map(group -> new GroupReference(group.getGroupUUID(), group.getName()));
}
/**
@@ -122,7 +176,7 @@
public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account account)
throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
addGroupMemberInReviewDb(db, groupUuid, account.getId());
- if (!writeGroupsToNoteDb) {
+ if (!groupsMigration.writeToNoteDb()) {
return;
}
addGroupMemberInNoteDb(groupUuid, account);
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 84698c8..3251c01 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -17,8 +17,9 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -26,7 +27,6 @@
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.SequencesOnInit;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountState;
@@ -46,6 +46,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Optional;
import org.apache.commons.validator.routines.EmailValidator;
public class InitAdminUser implements InitStep {
@@ -130,9 +131,18 @@
a.setPreferredEmail(email);
accounts.insert(a);
- AccountGroup adminGroup =
- groupsOnInit.getExistingGroup(db, new AccountGroup.NameKey("Administrators"));
- groupsOnInit.addGroupMember(db, adminGroup.getGroupUUID(), a);
+ // Only two groups should exist at this point in time and hence iterating over all of them
+ // is cheap.
+ Optional<GroupReference> adminGroupReference =
+ groupsOnInit
+ .getAllGroupReferences(db)
+ .filter(group -> group.getName().equals("Administrators"))
+ .findAny();
+ if (!adminGroupReference.isPresent()) {
+ throw new NoSuchGroupException("Administrators");
+ }
+ GroupReference adminGroup = adminGroupReference.get();
+ groupsOnInit.addGroupMember(db, adminGroup.getUUID(), a);
if (sshKey != null) {
VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
@@ -146,8 +156,7 @@
accountIndex.replace(as);
}
- InternalGroup adminInternalGroup =
- InternalGroup.create(adminGroup, ImmutableSet.of(id), ImmutableSet.of());
+ InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(db, adminGroup);
for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
groupIndex.replace(adminInternalGroup);
}
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index b59e085..afabcf6 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.config.GerritServerConfigModule;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.GitRepositoryManagerModule;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.schema.DataSourceModule;
import com.google.gerrit.server.schema.DataSourceProvider;
@@ -183,6 +184,7 @@
modules.add(new SchemaModule());
modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
modules.add(new NotesMigration.Module());
+ modules.add(new GroupsMigration.Module());
try {
return Guice.createInjector(PRODUCTION, modules);
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 2dd5b36..c7dc420 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -286,4 +286,25 @@
return Objects.hash(
name, groupId, description, visibleToAll, groupUUID, ownerGroupUUID, createdOn);
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{"
+ + "name="
+ + name
+ + ", groupId="
+ + groupId
+ + ", description="
+ + description
+ + ", visibleToAll="
+ + visibleToAll
+ + ", groupUUID="
+ + groupUUID
+ + ", ownerGroupUUID="
+ + ownerGroupUUID
+ + ", createdOn="
+ + createdOn
+ + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
index 30ca38f..17a205e 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -88,4 +88,9 @@
public int hashCode() {
return key.hashCode();
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{key=" + key + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
index 33955c4..5246d72 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -142,4 +142,19 @@
public int hashCode() {
return Objects.hash(key, addedBy, removedBy, removedOn);
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{"
+ + "key="
+ + key
+ + ", addedBy="
+ + addedBy
+ + ", removedBy="
+ + removedBy
+ + ", removedOn="
+ + removedOn
+ + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
index ea46366..e1e0754 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
@@ -84,4 +84,9 @@
public int hashCode() {
return key.hashCode();
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{key=" + key + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index 9968b7d..4ea19d2 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -147,4 +147,19 @@
public int hashCode() {
return Objects.hash(key, addedBy, removedBy, removedOn);
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{"
+ + "key="
+ + key
+ + ", addedBy="
+ + addedBy
+ + ", removedBy="
+ + removedBy
+ + ", removedOn="
+ + removedOn
+ + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/reviewdb/client/RefNames.java
index 8e9bc34..995b020 100644
--- a/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -85,6 +85,9 @@
/** NoteDb ref for a group {@code refs/groups} */
public static final String REFS_GROUPS = "refs/groups/";
+ /** NoteDb ref for the NoteMap of all group names */
+ public static final String REFS_GROUPNAMES = "refs/meta/group-names";
+
/** Draft inline comments of a user on a change */
public static final String REFS_DRAFT_COMMENTS = "refs/draft-comments/";
@@ -221,6 +224,10 @@
return ref.startsWith(REFS_USERS);
}
+ public static boolean isRefsGroups(String ref) {
+ return ref.startsWith(REFS_GROUPS);
+ }
+
static Integer parseShardedRefPart(String name) {
if (name == null) {
return null;
diff --git a/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java
new file mode 100644
index 0000000..640924c
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java
@@ -0,0 +1,310 @@
+// 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.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+public class DisallowReadFromGroupsReviewDbWrapper extends ReviewDbWrapper {
+ private static final String MSG = "This table has been migrated to NoteDb";
+
+ private final Groups groups;
+ private final GroupNames groupNames;
+ private final GroupMembers groupMembers;
+ private final GroupMemberAudits groupMemberAudits;
+ private final ByIds byIds;
+ private final ByIdAudits byIdAudits;
+
+ public DisallowReadFromGroupsReviewDbWrapper(ReviewDb db) {
+ super(db);
+ groups = new Groups(delegate.accountGroups());
+ groupNames = new GroupNames(delegate.accountGroupNames());
+ groupMembers = new GroupMembers(delegate.accountGroupMembers());
+ groupMemberAudits = new GroupMemberAudits(delegate.accountGroupMembersAudit());
+ byIds = new ByIds(delegate.accountGroupById());
+ byIdAudits = new ByIdAudits(delegate.accountGroupByIdAud());
+ }
+
+ public ReviewDb unsafeGetDelegate() {
+ return delegate;
+ }
+
+ @Override
+ public AccountGroupAccess accountGroups() {
+ return groups;
+ }
+
+ @Override
+ public AccountGroupNameAccess accountGroupNames() {
+ return groupNames;
+ }
+
+ @Override
+ public AccountGroupMemberAccess accountGroupMembers() {
+ return groupMembers;
+ }
+
+ @Override
+ public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
+ return groupMemberAudits;
+ }
+
+ @Override
+ public AccountGroupByIdAccess accountGroupById() {
+ return byIds;
+ }
+
+ @Override
+ public AccountGroupByIdAudAccess accountGroupByIdAud() {
+ return byIdAudits;
+ }
+
+ private static class Groups extends AccountGroupAccessWrapper {
+ protected Groups(AccountGroupAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroup, OrmException> getAsync(
+ AccountGroup.Id key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> get(Iterable<AccountGroup.Id> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroup get(AccountGroup.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> all() {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class GroupNames extends AccountGroupNameAccessWrapper {
+ protected GroupNames(AccountGroupNameAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupName, OrmException> getAsync(
+ AccountGroup.NameKey key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> get(Iterable<AccountGroup.NameKey> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupName get(AccountGroup.NameKey name) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> all() {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class GroupMembers extends AccountGroupMemberAccessWrapper {
+ protected GroupMembers(AccountGroupMemberAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMember, OrmException>
+ getAsync(AccountGroupMember.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> get(Iterable<AccountGroupMember.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupMember get(AccountGroupMember.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byAccount(Account.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class GroupMemberAudits extends AccountGroupMemberAuditAccessWrapper {
+ protected GroupMemberAudits(AccountGroupMemberAuditAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMemberAudit, OrmException>
+ getAsync(AccountGroupMemberAudit.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> get(Iterable<AccountGroupMemberAudit.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroupAccount(
+ AccountGroup.Id groupId, Account.Id accountId) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class ByIds extends AccountGroupByIdAccessWrapper {
+ protected ByIds(AccountGroupByIdAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupById, OrmException> getAsync(
+ AccountGroupById.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> get(Iterable<AccountGroupById.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupById get(AccountGroupById.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> all() {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class ByIdAudits extends AccountGroupByIdAudAccessWrapper {
+ protected ByIdAudits(AccountGroupByIdAudAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupByIdAud, OrmException>
+ getAsync(AccountGroupByIdAud.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> get(Iterable<AccountGroupByIdAud.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupByIdAud get(AccountGroupByIdAud.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroupInclude(
+ AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 739d2e6..22a9cf3 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -126,8 +126,4 @@
@Sequence(startWith = FIRST_CHANGE_ID)
@Deprecated
int nextChangeId() throws OrmException;
-
- default boolean changesTablesEnabled() {
- return true;
- }
}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
index bb31b1c..ef057eb 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
@@ -14,8 +14,16 @@
package com.google.gerrit.reviewdb.server;
+import static com.google.common.base.Preconditions.checkState;
+
import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.TreeSet;
/** Static utilities for ReviewDb types. */
public class ReviewDbUtil {
@@ -43,10 +51,30 @@
public static ReviewDb unwrapDb(ReviewDb db) {
if (db instanceof DisallowReadFromChangesReviewDbWrapper) {
- return ((DisallowReadFromChangesReviewDbWrapper) db).unsafeGetDelegate();
+ return unwrapDb(((DisallowReadFromChangesReviewDbWrapper) db).unsafeGetDelegate());
+ }
+ if (db instanceof DisallowReadFromGroupsReviewDbWrapper) {
+ return unwrapDb(((DisallowReadFromGroupsReviewDbWrapper) db).unsafeGetDelegate());
}
return db;
}
+ public static void checkColumns(Class<?> clazz, Integer... expected) {
+ Set<Integer> ids = new TreeSet<>();
+ for (Field f : clazz.getDeclaredFields()) {
+ Column col = f.getAnnotation(Column.class);
+ if (col != null) {
+ ids.add(col.id());
+ }
+ }
+ Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
+ checkState(
+ ids.equals(expectedIds),
+ "Unexpected column set for %s: %s != %s",
+ clazz.getSimpleName(),
+ ids,
+ expectedIds);
+ }
+
private ReviewDbUtil() {}
}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 7fd2c73..f0a8a34 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -17,6 +17,12 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -32,18 +38,38 @@
public class ReviewDbWrapper implements ReviewDb {
protected final ReviewDb delegate;
+ private boolean inTransaction;
+
protected ReviewDbWrapper(ReviewDb delegate) {
this.delegate = checkNotNull(delegate);
}
+ public boolean inTransaction() {
+ return inTransaction;
+ }
+
+ public void beginTransaction() {
+ inTransaction = true;
+ }
+
@Override
public void commit() throws OrmException {
- delegate.commit();
+ if (!inTransaction) {
+ // This reads a little weird, we're not in a transaction, so why are we calling commit?
+ // Because we want to let the underlying ReviewDb do its normal thing in this case (which may
+ // be throwing an exception, or not, depending on implementation).
+ delegate.commit();
+ }
}
@Override
public void rollback() throws OrmException {
- delegate.rollback();
+ if (inTransaction) {
+ inTransaction = false;
+ } else {
+ // See comment in commit(): we want to let the underlying ReviewDb do its thing.
+ delegate.rollback();
+ }
}
@Override
@@ -149,11 +175,6 @@
return delegate.nextChangeId();
}
- @Override
- public boolean changesTablesEnabled() {
- return delegate.changesTablesEnabled();
- }
-
public static class ChangeAccessWrapper implements ChangeAccess {
protected final ChangeAccess delegate;
@@ -679,4 +700,591 @@
return delegate.all();
}
}
+
+ public static class AccountGroupAccessWrapper implements AccountGroupAccess {
+ protected final AccountGroupAccess delegate;
+
+ protected AccountGroupAccessWrapper(AccountGroupAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroup> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroup.Id primaryKey(AccountGroup entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroup.Id, AccountGroup> toMap(Iterable<AccountGroup> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroup, OrmException> getAsync(
+ AccountGroup.Id key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> get(Iterable<AccountGroup.Id> keys) throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroup.Id> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroup.Id key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroup atomicUpdate(AccountGroup.Id key, AtomicUpdate<AccountGroup> update)
+ throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroup get(AccountGroup.Id id) throws OrmException {
+ return delegate.get(id);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException {
+ return delegate.byUUID(uuid);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> all() throws OrmException {
+ return delegate.all();
+ }
+ }
+
+ public static class AccountGroupNameAccessWrapper implements AccountGroupNameAccess {
+ protected final AccountGroupNameAccess delegate;
+
+ protected AccountGroupNameAccessWrapper(AccountGroupNameAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroup.NameKey primaryKey(AccountGroupName entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroup.NameKey, AccountGroupName> toMap(Iterable<AccountGroupName> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupName, OrmException> getAsync(
+ AccountGroup.NameKey key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> get(Iterable<AccountGroup.NameKey> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroup.NameKey> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroup.NameKey key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupName atomicUpdate(
+ AccountGroup.NameKey key, AtomicUpdate<AccountGroupName> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupName get(AccountGroup.NameKey name) throws OrmException {
+ return delegate.get(name);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> all() throws OrmException {
+ return delegate.all();
+ }
+ }
+
+ public static class AccountGroupMemberAccessWrapper implements AccountGroupMemberAccess {
+ protected final AccountGroupMemberAccess delegate;
+
+ protected AccountGroupMemberAccessWrapper(AccountGroupMemberAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupMember.Key primaryKey(AccountGroupMember entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupMember.Key, AccountGroupMember> toMap(Iterable<AccountGroupMember> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMember, OrmException>
+ getAsync(AccountGroupMember.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> get(Iterable<AccountGroupMember.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupMember.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupMember.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupMember atomicUpdate(
+ AccountGroupMember.Key key, AtomicUpdate<AccountGroupMember> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupMember get(AccountGroupMember.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException {
+ return delegate.byAccount(id);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException {
+ return delegate.byGroup(id);
+ }
+ }
+
+ public static class AccountGroupMemberAuditAccessWrapper
+ implements AccountGroupMemberAuditAccess {
+ protected final AccountGroupMemberAuditAccess delegate;
+
+ protected AccountGroupMemberAuditAccessWrapper(AccountGroupMemberAuditAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupMemberAudit.Key primaryKey(AccountGroupMemberAudit entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupMemberAudit.Key, AccountGroupMemberAudit> toMap(
+ Iterable<AccountGroupMemberAudit> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMemberAudit, OrmException>
+ getAsync(AccountGroupMemberAudit.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> get(Iterable<AccountGroupMemberAudit.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupMemberAudit.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupMemberAudit.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupMemberAudit atomicUpdate(
+ AccountGroupMemberAudit.Key key, AtomicUpdate<AccountGroupMemberAudit> update)
+ throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroupAccount(
+ AccountGroup.Id groupId, Account.Id accountId) throws OrmException {
+ return delegate.byGroupAccount(groupId, accountId);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) throws OrmException {
+ return delegate.byGroup(groupId);
+ }
+ }
+
+ public static class AccountGroupByIdAccessWrapper implements AccountGroupByIdAccess {
+ protected final AccountGroupByIdAccess delegate;
+
+ protected AccountGroupByIdAccessWrapper(AccountGroupByIdAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupById.Key primaryKey(AccountGroupById entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupById.Key, AccountGroupById> toMap(Iterable<AccountGroupById> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupById, OrmException> getAsync(
+ AccountGroupById.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> get(Iterable<AccountGroupById.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupById.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupById.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupById atomicUpdate(
+ AccountGroupById.Key key, AtomicUpdate<AccountGroupById> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupById get(AccountGroupById.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException {
+ return delegate.byIncludeUUID(uuid);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException {
+ return delegate.byGroup(id);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> all() throws OrmException {
+ return delegate.all();
+ }
+ }
+
+ public static class AccountGroupByIdAudAccessWrapper implements AccountGroupByIdAudAccess {
+ protected final AccountGroupByIdAudAccess delegate;
+
+ protected AccountGroupByIdAudAccessWrapper(AccountGroupByIdAudAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupByIdAud.Key primaryKey(AccountGroupByIdAud entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupByIdAud.Key, AccountGroupByIdAud> toMap(
+ Iterable<AccountGroupByIdAud> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupByIdAud, OrmException>
+ getAsync(AccountGroupByIdAud.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> get(Iterable<AccountGroupByIdAud.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupByIdAud.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupByIdAud.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupByIdAud atomicUpdate(
+ AccountGroupByIdAud.Key key, AtomicUpdate<AccountGroupByIdAud> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupByIdAud get(AccountGroupByIdAud.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroupInclude(
+ AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException {
+ return delegate.byGroupInclude(groupId, incGroupUUID);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) throws OrmException {
+ return delegate.byGroup(groupId);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index c222756..c56caac 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ScheduleConfig;
@@ -96,24 +97,42 @@
@Override
public void run() {
- log.debug("Running account deactivations");
+ log.info("Running account deactivations");
try {
int numberOfAccountsDeactivated = 0;
for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) {
- log.debug("processing account " + acc.getUserName());
- if (acc.getUserName() != null && !realm.isActive(acc.getUserName())) {
- sif.deactivate(acc.getAccount().getId());
- log.debug("deactivated accout " + acc.getUserName());
+ if (processAccount(acc)) {
numberOfAccountsDeactivated++;
}
}
log.info(
"Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated);
} catch (Exception e) {
- log.error("Failed to deactivate inactive accounts " + e.getMessage(), e);
+ log.error("Failed to complete deactivation of accounts: " + e.getMessage(), e);
}
}
+ private boolean processAccount(AccountState account) {
+ log.debug("processing account " + account.getUserName());
+ try {
+ if (account.getUserName() != null && !realm.isActive(account.getUserName())) {
+ sif.deactivate(account.getAccount().getId());
+ log.info("deactivated account " + account.getUserName());
+ return true;
+ }
+ } catch (ResourceConflictException e) {
+ log.info("Account {} already deactivated, continuing...", account.getUserName());
+ } catch (Exception e) {
+ log.error(
+ "Error deactivating account: {} ({}) {}",
+ account.getUserName(),
+ account.getAccount().getId(),
+ e.getMessage(),
+ e);
+ }
+ return false;
+ }
+
@Override
public String toString() {
return "account deactivator";
diff --git a/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java b/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
similarity index 96%
rename from java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
rename to java/com/google/gerrit/server/account/AccountExternalIdCreator.java
index 2f8dee6..8cf4ee0 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
+++ b/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.api.accounts;
+package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.externalids.ExternalId;
diff --git a/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java b/java/com/google/gerrit/server/account/AccountInfoComparator.java
similarity index 97%
rename from java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java
rename to java/com/google/gerrit/server/account/AccountInfoComparator.java
index 7c468fc..533dece 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java
+++ b/java/com/google/gerrit/server/account/AccountInfoComparator.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.api.accounts;
+package com.google.gerrit.server.account;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index a9428f0..8d39fb1 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -18,6 +18,7 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
@@ -56,6 +57,7 @@
this.emailValidator = emailValidator;
}
+ @Nullable
public Account get(Account.Id accountId) throws IOException, ConfigInvalidException {
try (Repository repo = repoManager.openRepository(allUsersName)) {
return read(repo, accountId);
@@ -133,6 +135,7 @@
}
}
+ @Nullable
private Account read(Repository allUsersRepository, Account.Id accountId)
throws IOException, ConfigInvalidException {
AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
diff --git a/java/com/google/gerrit/server/account/CreateAccount.java b/java/com/google/gerrit/server/account/CreateAccount.java
index a5d2e7a..ed92a7e 100644
--- a/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/account/CreateAccount.java
@@ -40,7 +40,6 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
-import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.group.UserInitiated;
import com.google.gerrit.server.group.db.GroupsUpdate;
diff --git a/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java b/java/com/google/gerrit/server/account/GpgApiAdapter.java
similarity index 93%
rename from java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
rename to java/com/google/gerrit/server/account/GpgApiAdapter.java
index 7def6fa..b060140 100644
--- a/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
+++ b/java/com/google/gerrit/server/account/GpgApiAdapter.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.api.accounts;
+package com.google.gerrit.server.account;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -21,7 +21,6 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResource;
import java.util.List;
import java.util.Map;
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 4bc8e64..4783f29 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -52,12 +52,15 @@
@Override
protected void configure() {
cache(BYID_NAME, AccountGroup.Id.class, new TypeLiteral<Optional<InternalGroup>>() {})
+ .maximumWeight(Long.MAX_VALUE)
.loader(ByIdLoader.class);
cache(BYNAME_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
+ .maximumWeight(Long.MAX_VALUE)
.loader(ByNameLoader.class);
cache(BYUUID_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
+ .maximumWeight(Long.MAX_VALUE)
.loader(ByUUIDLoader.class);
bind(GroupCacheImpl.class);
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 16655ab..119dc5b 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -144,6 +144,7 @@
return isOwner;
}
+ // Keep this logic in sync with VisibleRefFilter#isOwner(...).
if (group instanceof GroupDescription.Internal) {
AccountGroup.UUID ownerUUID = ((GroupDescription.Internal) group).getOwnerGroupUUID();
isOwner = getUser().getEffectiveGroups().contains(ownerUUID) || canAdministrateServer();
diff --git a/java/com/google/gerrit/server/account/GroupUUID.java b/java/com/google/gerrit/server/account/GroupUUID.java
index 45c7052..a7b32a1 100644
--- a/java/com/google/gerrit/server/account/GroupUUID.java
+++ b/java/com/google/gerrit/server/account/GroupUUID.java
@@ -25,6 +25,7 @@
MessageDigest md = Constants.newMessageDigest();
md.update(Constants.encode("group " + groupName + "\n"));
md.update(Constants.encode("creator " + creator.toExternalString() + "\n"));
+ md.update(Constants.encode(String.valueOf(Math.random())));
return new AccountGroup.UUID(ObjectId.fromRaw(md.digest()).name());
}
diff --git a/java/com/google/gerrit/server/account/InternalGroupBackend.java b/java/com/google/gerrit/server/account/InternalGroupBackend.java
index f2b9e68..5dc8b19 100644
--- a/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -22,7 +22,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.InternalGroupDescription;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.project.ProjectState;
@@ -30,7 +29,9 @@
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
/** Implementation of GroupBackend for the internal group system. */
@@ -75,24 +76,26 @@
public Collection<GroupReference> suggest(String name, ProjectState project) {
try (ReviewDb db = schema.open()) {
return groups
- .getAll(db)
- // TODO(aliceks): Filter the groups by name before loading them (if possible with NoteDb).
+ .getAllGroupReferences(db)
.filter(group -> startsWithIgnoreCase(group, name))
- .map(InternalGroupDescription::new)
.filter(this::isVisible)
- .map(GroupReference::forGroup)
.collect(toList());
- } catch (OrmException e) {
+ } catch (OrmException | IOException | ConfigInvalidException e) {
return ImmutableList.of();
}
}
- private static boolean startsWithIgnoreCase(InternalGroup group, String name) {
+ private static boolean startsWithIgnoreCase(GroupReference group, String name) {
return group.getName().regionMatches(true, 0, name, 0, name.length());
}
- private boolean isVisible(GroupDescription.Internal group) {
- return groupControlFactory.controlFor(group).isVisible();
+ private boolean isVisible(GroupReference groupReference) {
+ return groupCache
+ .get(groupReference.getUUID())
+ .map(InternalGroupDescription::new)
+ .map(groupControlFactory::controlFor)
+ .filter(GroupControl::isVisible)
+ .isPresent();
}
@Override
diff --git a/java/com/google/gerrit/server/account/QueryAccounts.java b/java/com/google/gerrit/server/account/QueryAccounts.java
index e6ac0f6..88f0bbc 100644
--- a/java/com/google/gerrit/server/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/account/QueryAccounts.java
@@ -28,7 +28,6 @@
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.query.account.AccountPredicates;
import com.google.gerrit.server.query.account.AccountQueryBuilder;
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
new file mode 100644
index 0000000..2741a0a
--- /dev/null
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -0,0 +1,21 @@
+java_library(
+ name = "api",
+ srcs = glob(
+ ["**/*.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib:gwtorm",
+ "//lib:servlet-api-3_1",
+ "//lib/guice",
+ "//lib/guice:guice-assistedinject",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/api/Module.java b/java/com/google/gerrit/server/api/GerritApiModule.java
similarity index 75%
rename from java/com/google/gerrit/server/api/Module.java
rename to java/com/google/gerrit/server/api/GerritApiModule.java
index 6214129..81226b0 100644
--- a/java/com/google/gerrit/server/api/Module.java
+++ b/java/com/google/gerrit/server/api/GerritApiModule.java
@@ -15,9 +15,12 @@
package com.google.gerrit.server.api;
import com.google.gerrit.extensions.api.GerritApi;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.extensions.api.plugins.Plugins;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.api.plugins.PluginApiImpl;
+import com.google.gerrit.server.api.plugins.PluginsImpl;
-public class Module extends AbstractModule {
+public class GerritApiModule extends FactoryModule {
@Override
protected void configure() {
bind(GerritApi.class).to(GerritApiImpl.class);
@@ -27,5 +30,8 @@
install(new com.google.gerrit.server.api.config.Module());
install(new com.google.gerrit.server.api.groups.Module());
install(new com.google.gerrit.server.api.projects.Module());
+
+ bind(Plugins.class).to(PluginsImpl.class);
+ factory(PluginApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index f36322c..7b14725 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -62,6 +62,7 @@
import com.google.gerrit.server.account.GetPreferences;
import com.google.gerrit.server.account.GetSshKeys;
import com.google.gerrit.server.account.GetWatchedProjects;
+import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gerrit.server.account.Index;
import com.google.gerrit.server.account.PostWatchedProjects;
import com.google.gerrit.server.account.PutActive;
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index 9490075..0438a23 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -129,6 +129,8 @@
}
lp.setFilterType(type);
+ lp.setAll(request.isAll());
+
return lp.apply();
}
diff --git a/java/com/google/gerrit/server/change/Abandon.java b/java/com/google/gerrit/server/change/Abandon.java
index c9d016d..c7addff 100644
--- a/java/com/google/gerrit/server/change/Abandon.java
+++ b/java/com/google/gerrit/server/change/Abandon.java
@@ -31,7 +31,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.AbandonOp;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/git/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
similarity index 98%
rename from java/com/google/gerrit/server/git/AbandonOp.java
rename to java/com/google/gerrit/server/change/AbandonOp.java
index 8298db3..cbe5e2b 100644
--- a/java/com/google/gerrit/server/git/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.change;
import com.google.common.base.Strings;
import com.google.common.collect.ListMultimap;
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 7563d99..a03f60a 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -105,9 +105,9 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 8f5989a..c272f6c 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,7 @@
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDeactivator;
+import com.google.gerrit.server.account.AccountExternalIdCreator;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibilityProvider;
@@ -90,13 +91,13 @@
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
-import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
import com.google.gerrit.server.avatar.AvatarProvider;
import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.change.AbandonOp;
import com.google.gerrit.server.change.AccountPatchReviewStore;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
@@ -108,7 +109,6 @@
import com.google.gerrit.server.events.UserScopedEventListener;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.AbandonOp;
import com.google.gerrit.server.git.ChangeMessageModifier;
import com.google.gerrit.server.git.EmailMerge;
import com.google.gerrit.server.git.GitModule;
@@ -300,7 +300,6 @@
bind(UiActions.class);
install(new com.google.gerrit.server.access.Module());
install(new com.google.gerrit.server.account.Module());
- install(new com.google.gerrit.server.api.Module());
install(new com.google.gerrit.server.change.Module());
install(new com.google.gerrit.server.config.Module());
install(new com.google.gerrit.server.group.Module());
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index fd512a5..b0f10f2 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -14,14 +14,22 @@
package com.google.gerrit.server.git;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.server.CommonConverters;
+import java.io.IOException;
import java.util.ArrayList;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
/** Static utilities for working with {@link RevCommit}s. */
public class CommitUtil {
- public static CommitInfo toCommitInfo(RevCommit commit) {
+ public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
+ return toCommitInfo(commit, null);
+ }
+
+ public static CommitInfo toCommitInfo(RevCommit commit, @Nullable RevWalk walk)
+ throws IOException {
CommitInfo info = new CommitInfo();
info.commit = commit.getName();
info.author = CommonConverters.toGitPerson(commit.getAuthorIdent());
@@ -30,7 +38,7 @@
info.message = commit.getFullMessage();
info.parents = new ArrayList<>(commit.getParentCount());
for (int i = 0; i < commit.getParentCount(); i++) {
- RevCommit p = commit.getParent(i);
+ RevCommit p = walk == null ? commit.getParent(i) : walk.parseCommit(commit.getParent(i));
CommitInfo parentInfo = new CommitInfo();
parentInfo.commit = p.getName();
parentInfo.subject = p.getShortMessage();
diff --git a/java/com/google/gerrit/server/git/MergeOp.java b/java/com/google/gerrit/server/git/MergeOp.java
index b6fbbe2..c9ea482 100644
--- a/java/com/google/gerrit/server/git/MergeOp.java
+++ b/java/com/google/gerrit/server/git/MergeOp.java
@@ -112,7 +112,7 @@
public class MergeOp implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
- private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.defaults().build();
+ private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.builder().build();
private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_ALLOW_CLOSED =
SUBMIT_RULE_OPTIONS.toBuilder().allowClosed(true).build();
diff --git a/java/com/google/gerrit/server/git/VersionedMetaData.java b/java/com/google/gerrit/server/git/VersionedMetaData.java
index 74a8134..812e693 100644
--- a/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
@@ -76,7 +77,9 @@
}
}
- protected RevCommit revision;
+ /** The revision at which the data was loaded. Is null for data yet to be created. */
+ @Nullable protected RevCommit revision;
+
protected RevWalk rw;
protected ObjectReader reader;
protected ObjectInserter inserter;
@@ -133,7 +136,8 @@
* @throws IOException
* @throws ConfigInvalidException
*/
- public void load(Repository db, ObjectId id) throws IOException, ConfigInvalidException {
+ public void load(Repository db, @Nullable ObjectId id)
+ throws IOException, ConfigInvalidException {
try (RevWalk walk = new RevWalk(db)) {
load(walk, id);
}
diff --git a/java/com/google/gerrit/server/git/VisibleRefFilter.java b/java/com/google/gerrit/server/git/VisibleRefFilter.java
index ed86a92..deede71 100644
--- a/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
@@ -23,6 +24,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@@ -30,6 +32,8 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
import com.google.gerrit.server.permissions.ChangePermission;
@@ -75,6 +79,7 @@
@Nullable private final SearchingChangeCacheImpl changeCache;
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> user;
+ private final GroupCache groupCache;
private final PermissionBackend permissionBackend;
private final PermissionBackend.ForProject perm;
private final ProjectState projectState;
@@ -90,6 +95,7 @@
@Nullable SearchingChangeCacheImpl changeCache,
Provider<ReviewDb> db,
Provider<CurrentUser> user,
+ GroupCache groupCache,
PermissionBackend permissionBackend,
@Assisted ProjectState projectState,
@Assisted Repository git) {
@@ -98,6 +104,7 @@
this.changeCache = changeCache;
this.db = db;
this.user = user;
+ this.groupCache = groupCache;
this.permissionBackend = permissionBackend;
this.perm =
permissionBackend.user(user).database(db).project(projectState.getProject().getNameKey());
@@ -118,22 +125,29 @@
PermissionBackend.WithUser withUser = permissionBackend.user(user);
PermissionBackend.ForProject forProject = withUser.project(projectState.getNameKey());
- if (checkProjectPermission(forProject, ProjectPermission.READ)) {
- return refs;
- } else if (checkProjectPermission(forProject, ProjectPermission.READ_NO_CONFIG)) {
- return fastHideRefsMetaConfig(refs);
+ if (!projectState.isAllUsers()) {
+ if (checkProjectPermission(forProject, ProjectPermission.READ)) {
+ return refs;
+ } else if (checkProjectPermission(forProject, ProjectPermission.READ_NO_CONFIG)) {
+ return fastHideRefsMetaConfig(refs);
+ }
}
- Account.Id userId;
boolean viewMetadata;
+ boolean isAdmin;
+ Account.Id userId;
+ IdentifiedUser identifiedUser;
if (user.get().isIdentifiedUser()) {
viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
- IdentifiedUser u = user.get().asIdentifiedUser();
- userId = u.getAccountId();
+ isAdmin = withUser.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
+ identifiedUser = user.get().asIdentifiedUser();
+ userId = identifiedUser.getAccountId();
userEditPrefix = RefNames.refsEditPrefix(userId);
} else {
- userId = null;
viewMetadata = false;
+ isAdmin = false;
+ userId = null;
+ identifiedUser = null;
}
Map<String, Ref> result = new HashMap<>();
@@ -143,6 +157,7 @@
String name = ref.getName();
Change.Id changeId;
Account.Id accountId;
+ AccountGroup.UUID accountGroupUuid;
if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) {
continue;
} else if (RefNames.isRefsEdit(name)) {
@@ -156,10 +171,19 @@
result.put(name, ref);
}
} else if ((accountId = Account.Id.fromRef(name)) != null) {
- // Account ref is visible only to corresponding account.
+ // Account ref is visible only to the corresponding account.
if (viewMetadata || (accountId.equals(userId) && canReadRef(name))) {
result.put(name, ref);
}
+ } else if ((accountGroupUuid = AccountGroup.UUID.fromRef(name)) != null) {
+ // Group ref is visible only to the corresponding owner group.
+ InternalGroup group = groupCache.get(accountGroupUuid).orElse(null);
+ if (viewMetadata
+ || (group != null
+ && isGroupOwner(group, identifiedUser, isAdmin)
+ && canReadRef(name))) {
+ result.put(name, ref);
+ }
} else if (isTag(ref)) {
// If its a tag, consider it later.
if (ref.getObjectId() != null) {
@@ -170,8 +194,10 @@
if (viewMetadata) {
result.put(name, ref);
}
- } else if (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS)) {
- // The notes branch with the external IDs of all users must not be exposed to normal users.
+ } else if (projectState.isAllUsers()
+ && (name.equals(RefNames.REFS_EXTERNAL_IDS) || name.equals(RefNames.REFS_GROUPNAMES))) {
+ // The notes branches with the external IDs / group names must not be exposed to normal
+ // users.
if (viewMetadata) {
result.put(name, ref);
}
@@ -180,6 +206,12 @@
// symbolic we want the control around the final target. If its
// not symbolic then getLeaf() is a no-op returning ref itself.
result.put(name, ref);
+ } else if (isRefsUsersSelf(ref)) {
+ // viewMetadata allows to see all account refs, hence refs/users/self should be included as
+ // well
+ if (viewMetadata) {
+ result.put(name, ref);
+ }
}
}
@@ -327,15 +359,17 @@
}
private boolean isMetadata(String name) {
- return name.startsWith(REFS_CHANGES)
- || RefNames.isRefsEdit(name)
- || (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS));
+ return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name);
}
private static boolean isTag(Ref ref) {
return ref.getLeaf().getName().startsWith(Constants.R_TAGS);
}
+ private static boolean isRefsUsersSelf(Ref ref) {
+ return ref.getName().startsWith(REFS_USERS_SELF);
+ }
+
private boolean canReadRef(String ref) {
try {
perm.ref(ref).check(RefPermission.READ);
@@ -364,4 +398,13 @@
}
return true;
}
+
+ private boolean isGroupOwner(
+ InternalGroup group, @Nullable IdentifiedUser user, boolean isAdmin) {
+ checkNotNull(group);
+
+ // Keep this logic in sync with GroupControl#isOwner().
+ return isAdmin
+ || (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
+ }
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 417bd26..b9af127 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -778,7 +778,7 @@
}
}
- /** Rejects updates to group branches. */
+ /** Rejects updates to group branches (refs/groups/* and refs/meta/group-names). */
public static class GroupCommitValidator implements CommitValidationListener {
private final AllUsersName allUsers;
@@ -790,6 +790,7 @@
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
// Groups are stored inside 'refs/groups/' refs inside the 'All-Users' repository.
+ // Group names are stored inside 'refs/meta/group-names' refs inside the 'All-Users' repository.
if (!allUsers.equals(receiveEvent.project.getNameKey())) {
return Collections.emptyList();
}
@@ -800,7 +801,8 @@
return Collections.emptyList();
}
- if (receiveEvent.command.getRefName().startsWith(RefNames.REFS_GROUPS)) {
+ if (receiveEvent.command.getRefName().startsWith(RefNames.REFS_GROUPS)
+ || receiveEvent.command.getRefName().equals(RefNames.REFS_GROUPNAMES)) {
throw new CommitValidationException("group update not allowed");
}
return Collections.emptyList();
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index ee6f387..22335b6 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -319,8 +319,10 @@
IdentifiedUser caller)
throws MergeValidationException {
// Groups are stored inside 'refs/groups/' refs inside the 'All-Users' repository.
+ // Group names are stored inside 'refs/meta/group-names' inside the 'All-Users' repository.
if (!allUsersName.equals(destProject.getNameKey())
- || !destBranch.get().startsWith(RefNames.REFS_GROUPS)) {
+ || (!destBranch.get().startsWith(RefNames.REFS_GROUPS)
+ && !destBranch.get().equals(RefNames.REFS_GROUPNAMES))) {
return;
}
diff --git a/java/com/google/gerrit/server/group/AddMembers.java b/java/com/google/gerrit/server/group/AddMembers.java
index 822a36e..b57a462 100644
--- a/java/com/google/gerrit/server/group/AddMembers.java
+++ b/java/com/google/gerrit/server/group/AddMembers.java
@@ -48,7 +48,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -122,7 +122,7 @@
throw new AuthException("Cannot add members to group " + internalGroup.getName());
}
- Set<Account.Id> newMemberIds = new HashSet<>();
+ Set<Account.Id> newMemberIds = new LinkedHashSet<>();
for (String nameOrEmailOrId : input.members) {
Account a = findAccount(nameOrEmailOrId);
if (!a.isActive()) {
diff --git a/java/com/google/gerrit/server/group/AddSubgroups.java b/java/com/google/gerrit/server/group/AddSubgroups.java
index 73bea25..af0657a 100644
--- a/java/com/google/gerrit/server/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/group/AddSubgroups.java
@@ -39,7 +39,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -102,7 +102,7 @@
}
List<GroupInfo> result = new ArrayList<>();
- Set<AccountGroup.UUID> subgroupUuids = new HashSet<>();
+ Set<AccountGroup.UUID> subgroupUuids = new LinkedHashSet<>();
for (String subgroupIdentifier : input.groups) {
GroupDescription.Basic subgroup = groupsCollection.parse(subgroupIdentifier);
subgroupUuids.add(subgroup.getGroupUUID());
diff --git a/java/com/google/gerrit/server/group/GetAuditLog.java b/java/com/google/gerrit/server/group/GetAuditLog.java
index ebada0b..52a9dd9 100644
--- a/java/com/google/gerrit/server/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/group/GetAuditLog.java
@@ -31,44 +31,56 @@
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.db.Groups;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
@Singleton
public class GetAuditLog implements RestReadView<GroupResource> {
private final Provider<ReviewDb> db;
private final AccountLoader.Factory accountLoaderFactory;
+ private final AllUsersName allUsers;
private final GroupCache groupCache;
private final GroupJson groupJson;
private final GroupBackend groupBackend;
private final Groups groups;
+ private final GitRepositoryManager repoManager;
@Inject
public GetAuditLog(
Provider<ReviewDb> db,
AccountLoader.Factory accountLoaderFactory,
+ AllUsersName allUsers,
GroupCache groupCache,
GroupJson groupJson,
GroupBackend groupBackend,
- Groups groups) {
+ Groups groups,
+ GitRepositoryManager repoManager) {
this.db = db;
this.accountLoaderFactory = accountLoaderFactory;
+ this.allUsers = allUsers;
this.groupCache = groupCache;
this.groupJson = groupJson;
this.groupBackend = groupBackend;
this.groups = groups;
+ this.repoManager = repoManager;
}
@Override
public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
- throws AuthException, MethodNotAllowedException, OrmException {
+ throws AuthException, MethodNotAllowedException, OrmException, IOException,
+ ConfigInvalidException {
GroupDescription.Internal group =
rsrc.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
if (!rsrc.getControl().isOwner()) {
@@ -79,53 +91,54 @@
List<GroupAuditEventInfo> auditEvents = new ArrayList<>();
- for (AccountGroupMemberAudit auditEvent :
- groups.getMembersAudit(db.get(), group.getGroupUUID())) {
- AccountInfo member = accountLoader.get(auditEvent.getMemberId());
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+ for (AccountGroupMemberAudit auditEvent :
+ groups.getMembersAudit(db.get(), allUsersRepo, group.getGroupUUID())) {
+ AccountInfo member = accountLoader.get(auditEvent.getMemberId());
- auditEvents.add(
- GroupAuditEventInfo.createAddUserEvent(
- accountLoader.get(auditEvent.getAddedBy()), auditEvent.getAddedOn(), member));
-
- if (!auditEvent.isActive()) {
auditEvents.add(
- GroupAuditEventInfo.createRemoveUserEvent(
- accountLoader.get(auditEvent.getRemovedBy()), auditEvent.getRemovedOn(), member));
- }
- }
+ GroupAuditEventInfo.createAddUserEvent(
+ accountLoader.get(auditEvent.getAddedBy()), auditEvent.getAddedOn(), member));
- for (AccountGroupByIdAud auditEvent :
- groups.getSubgroupsAudit(db.get(), group.getGroupUUID())) {
- AccountGroup.UUID includedGroupUUID = auditEvent.getIncludeUUID();
- Optional<InternalGroup> includedGroup = groupCache.get(includedGroupUUID);
- GroupInfo member;
- if (includedGroup.isPresent()) {
- member = groupJson.format(new InternalGroupDescription(includedGroup.get()));
- } else {
- GroupDescription.Basic groupDescription = groupBackend.get(includedGroupUUID);
- member = new GroupInfo();
- member.id = Url.encode(includedGroupUUID.get());
- member.name = groupDescription.getName();
+ if (!auditEvent.isActive()) {
+ auditEvents.add(
+ GroupAuditEventInfo.createRemoveUserEvent(
+ accountLoader.get(auditEvent.getRemovedBy()), auditEvent.getRemovedOn(), member));
+ }
}
- auditEvents.add(
- GroupAuditEventInfo.createAddGroupEvent(
- accountLoader.get(auditEvent.getAddedBy()),
- auditEvent.getKey().getAddedOn(),
- member));
+ for (AccountGroupByIdAud auditEvent :
+ groups.getSubgroupsAudit(db.get(), allUsersRepo, group.getGroupUUID())) {
+ AccountGroup.UUID includedGroupUUID = auditEvent.getIncludeUUID();
+ Optional<InternalGroup> includedGroup = groupCache.get(includedGroupUUID);
+ GroupInfo member;
+ if (includedGroup.isPresent()) {
+ member = groupJson.format(new InternalGroupDescription(includedGroup.get()));
+ } else {
+ GroupDescription.Basic groupDescription = groupBackend.get(includedGroupUUID);
+ member = new GroupInfo();
+ member.id = Url.encode(includedGroupUUID.get());
+ member.name = groupDescription.getName();
+ }
- if (!auditEvent.isActive()) {
auditEvents.add(
- GroupAuditEventInfo.createRemoveGroupEvent(
- accountLoader.get(auditEvent.getRemovedBy()), auditEvent.getRemovedOn(), member));
+ GroupAuditEventInfo.createAddGroupEvent(
+ accountLoader.get(auditEvent.getAddedBy()),
+ auditEvent.getKey().getAddedOn(),
+ member));
+
+ if (!auditEvent.isActive()) {
+ auditEvents.add(
+ GroupAuditEventInfo.createRemoveGroupEvent(
+ accountLoader.get(auditEvent.getRemovedBy()), auditEvent.getRemovedOn(), member));
+ }
}
}
accountLoader.fill();
- // sort by date in reverse order so that the newest audit event comes first
+ // sort by date and then reverse so that the newest audit event comes first
Collections.sort(auditEvents, comparing((GroupAuditEventInfo a) -> a.date).reversed());
-
return auditEvents;
}
}
diff --git a/java/com/google/gerrit/server/group/InternalGroup.java b/java/com/google/gerrit/server/group/InternalGroup.java
index ad6fa5c..7828586 100644
--- a/java/com/google/gerrit/server/group/InternalGroup.java
+++ b/java/com/google/gerrit/server/group/InternalGroup.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import java.io.Serializable;
import java.sql.Timestamp;
+import org.eclipse.jgit.lib.ObjectId;
@AutoValue
public abstract class InternalGroup implements Serializable {
@@ -30,6 +31,14 @@
AccountGroup accountGroup,
ImmutableSet<Account.Id> members,
ImmutableSet<AccountGroup.UUID> subgroups) {
+ return create(accountGroup, members, subgroups, null);
+ }
+
+ public static InternalGroup create(
+ AccountGroup accountGroup,
+ ImmutableSet<Account.Id> members,
+ ImmutableSet<AccountGroup.UUID> subgroups,
+ ObjectId refState) {
return builder()
.setId(accountGroup.getId())
.setNameKey(accountGroup.getNameKey())
@@ -40,6 +49,7 @@
.setCreatedOn(accountGroup.getCreatedOn())
.setMembers(members)
.setSubgroups(subgroups)
+ .setRefState(refState)
.build();
}
@@ -66,6 +76,11 @@
public abstract ImmutableSet<AccountGroup.UUID> getSubgroups();
+ @Nullable
+ public abstract ObjectId getRefState();
+
+ public abstract Builder toBuilder();
+
public static Builder builder() {
return new AutoValue_InternalGroup.Builder();
}
@@ -90,6 +105,8 @@
public abstract Builder setSubgroups(ImmutableSet<AccountGroup.UUID> subgroups);
+ public abstract Builder setRefState(ObjectId refState);
+
public abstract InternalGroup build();
}
}
diff --git a/java/com/google/gerrit/server/group/ListGroups.java b/java/com/google/gerrit/server/group/ListGroups.java
index 5c532e4..77778f3 100644
--- a/java/com/google/gerrit/server/group/ListGroups.java
+++ b/java/com/google/gerrit/server/group/ListGroups.java
@@ -46,6 +46,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.Collection;
import java.util.Comparator;
@@ -53,12 +54,14 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.Option;
/** List groups visible to the calling user. */
@@ -257,7 +260,7 @@
@Override
public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
- throws OrmException, RestApiException {
+ throws OrmException, RestApiException, IOException, ConfigInvalidException {
SortedMap<String, GroupInfo> output = new TreeMap<>();
for (GroupInfo info : get()) {
output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
@@ -266,7 +269,8 @@
return output;
}
- public List<GroupInfo> get() throws OrmException, RestApiException {
+ public List<GroupInfo> get()
+ throws OrmException, RestApiException, IOException, ConfigInvalidException {
if (!Strings.isNullOrEmpty(suggest)) {
return suggestGroups();
}
@@ -290,13 +294,14 @@
return getAllGroups();
}
- private List<GroupInfo> getAllGroups() throws OrmException {
+ private List<GroupInfo> getAllGroups() throws OrmException, IOException, ConfigInvalidException {
Pattern pattern = getRegexPattern();
Stream<GroupDescription.Internal> existingGroups =
getAllExistingGroups()
- // TODO(aliceks): Filter groups by UUID/name before loading them (if possible with
- // NoteDb).
- .filter(group -> !isNotRelevant(pattern, group))
+ .filter(group -> isRelevant(pattern, group))
+ .map(this::loadGroup)
+ .flatMap(Streams::stream)
+ .filter(this::isVisible)
.sorted(GROUP_COMPARATOR)
.skip(start);
if (limit > 0) {
@@ -310,19 +315,16 @@
return groupInfos;
}
- private Stream<GroupDescription.Internal> getAllExistingGroups() throws OrmException {
+ private Stream<GroupReference> getAllExistingGroups()
+ throws OrmException, IOException, ConfigInvalidException {
if (!projects.isEmpty()) {
return projects
.stream()
.map(ProjectState::getAllGroups)
.flatMap(Collection::stream)
- .map(GroupReference::getUUID)
- .distinct()
- .map(groupCache::get)
- .flatMap(Streams::stream)
- .map(InternalGroupDescription::new);
+ .distinct();
}
- return groups.getAll(db.get()).map(InternalGroupDescription::new);
+ return groups.getAllGroupReferences(db.get());
}
private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
@@ -381,15 +383,15 @@
}
private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
- throws OrmException {
+ throws OrmException, IOException, ConfigInvalidException {
Pattern pattern = getRegexPattern();
Stream<? extends GroupDescription.Internal> foundGroups =
groups
- .getAll(db.get())
- .map(InternalGroupDescription::new)
- // TODO(aliceks): Filter groups by UUID/name before loading them (if possible with
- // NoteDb).
- .filter(group -> !isNotRelevant(pattern, group))
+ .getAllGroupReferences(db.get())
+ .filter(group -> isRelevant(pattern, group))
+ .map(this::loadGroup)
+ .flatMap(Streams::stream)
+ .filter(this::isVisible)
.filter(filter)
.sorted(GROUP_COMPARATOR)
.skip(start);
@@ -404,12 +406,18 @@
return groupInfos;
}
- private List<GroupInfo> getGroupsOwnedBy(String id) throws OrmException, RestApiException {
+ private Optional<GroupDescription.Internal> loadGroup(GroupReference groupReference) {
+ return groupCache.get(groupReference.getUUID()).map(InternalGroupDescription::new);
+ }
+
+ private List<GroupInfo> getGroupsOwnedBy(String id)
+ throws OrmException, RestApiException, IOException, ConfigInvalidException {
String uuid = groupsCollection.parse(id).getGroupUUID().get();
return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
}
- private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user) throws OrmException {
+ private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
+ throws OrmException, IOException, ConfigInvalidException {
return filterGroupsOwnedBy(group -> isOwner(user, group));
}
@@ -425,24 +433,24 @@
return Strings.isNullOrEmpty(matchRegex) ? null : Pattern.compile(matchRegex);
}
- private boolean isNotRelevant(Pattern pattern, GroupDescription.Internal group) {
+ private boolean isRelevant(Pattern pattern, GroupReference group) {
if (!Strings.isNullOrEmpty(matchSubstring)) {
if (!group.getName().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US))) {
- return true;
+ return false;
}
} else if (pattern != null) {
if (!pattern.matcher(group.getName()).matches()) {
- return true;
+ return false;
}
}
- if (visibleToAll && !group.isVisibleToAll()) {
- return true;
- }
- if (!groupsToInspect.isEmpty() && !groupsToInspect.contains(group.getGroupUUID())) {
- return true;
- }
+ return groupsToInspect.isEmpty() || groupsToInspect.contains(group.getUUID());
+ }
+ private boolean isVisible(GroupDescription.Internal group) {
+ if (visibleToAll && !group.isVisibleToAll()) {
+ return false;
+ }
GroupControl c = groupControlFactory.controlFor(group);
- return !c.isVisible();
+ return c.isVisible();
}
}
diff --git a/java/com/google/gerrit/server/group/ListMembers.java b/java/com/google/gerrit/server/group/ListMembers.java
index 2d069f8..fa33957 100644
--- a/java/com/google/gerrit/server/group/ListMembers.java
+++ b/java/com/google/gerrit/server/group/ListMembers.java
@@ -26,10 +26,10 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index e0884cf..91cc11c 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -38,6 +38,7 @@
import com.google.gwtorm.server.SchemaFactory;
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.Collections;
@@ -49,6 +50,7 @@
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -213,31 +215,29 @@
return;
}
- Optional<InternalGroup> conflictingGroup;
+ Optional<GroupReference> conflictingGroup;
try (ReviewDb db = schema.open()) {
conflictingGroup =
groups
- .getAll(db)
- // TODO(aliceks): Filter the groups by name as early as possible and avoid loading
- // them (if possible with NoteDb).
+ .getAllGroupReferences(db)
.filter(group -> hasConfiguredName(byLowerCaseConfiguredName, group))
.findAny();
- } catch (OrmException ignored) {
+ } catch (OrmException | IOException | ConfigInvalidException ignored) {
return;
}
if (conflictingGroup.isPresent()) {
- InternalGroup group = conflictingGroup.get();
+ GroupReference group = conflictingGroup.get();
String groupName = group.getName();
AccountGroup.UUID systemGroupUuid = byLowerCaseConfiguredName.get(groupName);
throw new StartupException(
- getAmbiguousNameMessage(groupName, group.getGroupUUID(), systemGroupUuid));
+ getAmbiguousNameMessage(groupName, group.getUUID(), systemGroupUuid));
}
}
private static boolean hasConfiguredName(
- Map<String, AccountGroup.UUID> byLowerCaseConfiguredName, InternalGroup group) {
+ Map<String, AccountGroup.UUID> byLowerCaseConfiguredName, GroupReference group) {
String name = group.getName().toLowerCase(Locale.US);
return byLowerCaseConfiguredName.keySet().contains(name);
}
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
new file mode 100644
index 0000000..c7add92
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -0,0 +1,259 @@
+// 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.group.db;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.notedb.NoteDbUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** NoteDb reader for group audit log. */
+@Singleton
+class AuditLogReader {
+ private static final Logger log = LoggerFactory.getLogger(AuditLogReader.class);
+
+ private final String serverId;
+
+ @Inject
+ AuditLogReader(@GerritServerId String serverId) {
+ this.serverId = serverId;
+ }
+
+ // Having separate methods for reading the two types of audit records mirrors the split in
+ // ReviewDb. Once ReviewDb is gone, the audit record interface becomes more flexible and we can
+ // revisit this, e.g. to do only a single walk, or even change the record types.
+
+ ImmutableList<AccountGroupMemberAudit> getMembersAudit(Repository repo, AccountGroup.UUID uuid)
+ throws IOException, ConfigInvalidException {
+ return getMembersAudit(getGroupId(repo, uuid), parseCommits(repo, uuid));
+ }
+
+ private ImmutableList<AccountGroupMemberAudit> getMembersAudit(
+ AccountGroup.Id groupId, List<ParsedCommit> commits) {
+ ListMultimap<MemberKey, AccountGroupMemberAudit> audits =
+ MultimapBuilder.hashKeys().linkedListValues().build();
+ ImmutableList.Builder<AccountGroupMemberAudit> result = ImmutableList.builder();
+ for (ParsedCommit pc : commits) {
+ for (Account.Id id : pc.addedMembers()) {
+ MemberKey key = MemberKey.create(groupId, id);
+ AccountGroupMemberAudit audit =
+ new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
+ audits.put(key, audit);
+ result.add(audit);
+ }
+ for (Account.Id id : pc.removedMembers()) {
+ List<AccountGroupMemberAudit> adds = audits.get(MemberKey.create(groupId, id));
+ if (!adds.isEmpty()) {
+ AccountGroupMemberAudit audit = adds.remove(0);
+ audit.removed(pc.authorId(), pc.when());
+ } else {
+ // Match old behavior of DbGroupMemberAuditListener and add a "legacy" add/remove pair.
+ AccountGroupMemberAudit audit =
+ new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
+ audit.removedLegacy();
+ result.add(audit);
+ }
+ }
+ }
+ return result.build();
+ }
+
+ ImmutableList<AccountGroupByIdAud> getSubgroupsAudit(Repository repo, AccountGroup.UUID uuid)
+ throws IOException, ConfigInvalidException {
+ return getSubgroupsAudit(getGroupId(repo, uuid), parseCommits(repo, uuid));
+ }
+
+ private ImmutableList<AccountGroupByIdAud> getSubgroupsAudit(
+ AccountGroup.Id groupId, List<ParsedCommit> commits) {
+ ListMultimap<SubgroupKey, AccountGroupByIdAud> audits =
+ MultimapBuilder.hashKeys().linkedListValues().build();
+ ImmutableList.Builder<AccountGroupByIdAud> result = ImmutableList.builder();
+ for (ParsedCommit pc : commits) {
+ for (AccountGroup.UUID uuid : pc.addedSubgroups()) {
+ SubgroupKey key = SubgroupKey.create(groupId, uuid);
+ AccountGroupByIdAud audit =
+ new AccountGroupByIdAud(
+ new AccountGroupByIdAud.Key(groupId, uuid, pc.when()), pc.authorId());
+ audits.put(key, audit);
+ result.add(audit);
+ }
+ for (AccountGroup.UUID uuid : pc.removedSubgroups()) {
+ List<AccountGroupByIdAud> adds = audits.get(SubgroupKey.create(groupId, uuid));
+ if (!adds.isEmpty()) {
+ AccountGroupByIdAud audit = adds.remove(0);
+ audit.removed(pc.authorId(), pc.when());
+ } else {
+ // Unlike members, DbGroupMemberAuditListener didn't insert an add/remove pair here.
+ }
+ }
+ }
+ return result.build();
+ }
+
+ private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
+ Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent(), serverId);
+ if (!authorId.isPresent()) {
+ // Only report audit events from identified users, since this is a non-nullable field in
+ // ReviewDb. May be revisited after groups are fully migrated to NoteDb.
+ return Optional.empty();
+ }
+
+ List<Account.Id> addedMembers = new ArrayList<>();
+ List<AccountGroup.UUID> addedSubgroups = new ArrayList<>();
+ List<Account.Id> removedMembers = new ArrayList<>();
+ List<AccountGroup.UUID> removedSubgroups = new ArrayList<>();
+
+ for (FooterLine line : c.getFooterLines()) {
+ if (line.matches(GroupConfig.FOOTER_ADD_MEMBER)) {
+ parseAccount(uuid, c, line).ifPresent(addedMembers::add);
+ } else if (line.matches(GroupConfig.FOOTER_REMOVE_MEMBER)) {
+ parseAccount(uuid, c, line).ifPresent(removedMembers::add);
+ } else if (line.matches(GroupConfig.FOOTER_ADD_GROUP)) {
+ parseGroup(uuid, c, line).ifPresent(addedSubgroups::add);
+ } else if (line.matches(GroupConfig.FOOTER_REMOVE_GROUP)) {
+ parseGroup(uuid, c, line).ifPresent(removedSubgroups::add);
+ }
+ }
+ return Optional.of(
+ new AutoValue_AuditLogReader_ParsedCommit(
+ authorId.get(),
+ new Timestamp(c.getAuthorIdent().getWhen().getTime()),
+ ImmutableList.copyOf(addedMembers),
+ ImmutableList.copyOf(removedMembers),
+ ImmutableList.copyOf(addedSubgroups),
+ ImmutableList.copyOf(removedSubgroups)));
+ }
+
+ private Optional<Account.Id> parseAccount(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
+ Optional<Account.Id> result =
+ Optional.ofNullable(RawParseUtils.parsePersonIdent(line.getValue()))
+ .flatMap(ident -> NoteDbUtil.parseIdent(ident, serverId));
+ if (!result.isPresent()) {
+ logInvalid(uuid, c, line);
+ }
+ return result;
+ }
+
+ private static Optional<AccountGroup.UUID> parseGroup(
+ AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
+ PersonIdent ident = RawParseUtils.parsePersonIdent(line.getValue());
+ if (ident == null) {
+ logInvalid(uuid, c, line);
+ return Optional.empty();
+ }
+ return Optional.of(new AccountGroup.UUID(ident.getEmailAddress()));
+ }
+
+ private static void logInvalid(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
+ log.debug(
+ "Invalid footer line in commit {} while parsing audit log for group {}: {}",
+ c.name(),
+ uuid,
+ line);
+ }
+
+ private ImmutableList<ParsedCommit> parseCommits(Repository repo, AccountGroup.UUID uuid)
+ throws IOException {
+ try (RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
+ if (ref == null) {
+ return ImmutableList.of();
+ }
+
+ rw.reset();
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ rw.setRetainBody(true);
+ rw.sort(RevSort.COMMIT_TIME_DESC, true);
+ rw.sort(RevSort.REVERSE, true);
+
+ ImmutableList.Builder<ParsedCommit> result = ImmutableList.builder();
+ RevCommit c;
+ while ((c = rw.next()) != null) {
+ parse(uuid, c).ifPresent(result::add);
+ }
+ return result.build();
+ }
+ }
+
+ private AccountGroup.Id getGroupId(Repository repo, AccountGroup.UUID uuid)
+ throws ConfigInvalidException, IOException {
+ // TODO(dborowitz): This re-walks all commits just to find createdOn, which we don't need.
+ return GroupConfig.loadForGroup(repo, uuid).getLoadedGroup().get().getId();
+ }
+
+ @AutoValue
+ abstract static class MemberKey {
+ static MemberKey create(AccountGroup.Id groupId, Account.Id memberId) {
+ return new AutoValue_AuditLogReader_MemberKey(groupId, memberId);
+ }
+
+ abstract AccountGroup.Id groupId();
+
+ abstract Account.Id memberId();
+ }
+
+ @AutoValue
+ abstract static class SubgroupKey {
+ static SubgroupKey create(AccountGroup.Id groupId, AccountGroup.UUID subgroupUuid) {
+ return new AutoValue_AuditLogReader_SubgroupKey(groupId, subgroupUuid);
+ }
+
+ abstract AccountGroup.Id groupId();
+
+ abstract AccountGroup.UUID subgroupUuid();
+ }
+
+ @AutoValue
+ abstract static class ParsedCommit {
+ abstract Account.Id authorId();
+
+ abstract Timestamp when();
+
+ abstract ImmutableList<Account.Id> addedMembers();
+
+ abstract ImmutableList<Account.Id> removedMembers();
+
+ abstract ImmutableList<AccountGroup.UUID> addedSubgroups();
+
+ abstract ImmutableList<AccountGroup.UUID> removedSubgroups();
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/GroupBundle.java b/java/com/google/gerrit/server/group/db/GroupBundle.java
new file mode 100644
index 0000000..642a6bd
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/GroupBundle.java
@@ -0,0 +1,235 @@
+// 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.group.db;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * A bundle of all entities rooted at a single {@link AccountGroup} entity.
+ *
+ * <p>Used primarily during the migration process. Most callers should prefer {@link InternalGroup}
+ * instead.
+ */
+@AutoValue
+public abstract class GroupBundle {
+ static {
+ // Initialization-time checks that the column set hasn't changed since the
+ // last time this file was updated.
+ checkColumns(AccountGroup.NameKey.class, 1);
+ checkColumns(AccountGroup.UUID.class, 1);
+ checkColumns(AccountGroup.Id.class, 1);
+ checkColumns(AccountGroup.class, 1, 2, 4, 7, 9, 10, 11);
+
+ checkColumns(AccountGroupById.Key.class, 1, 2);
+ checkColumns(AccountGroupById.class, 1);
+
+ checkColumns(AccountGroupByIdAud.Key.class, 1, 2, 3);
+ checkColumns(AccountGroupByIdAud.class, 1, 2, 3, 4);
+
+ checkColumns(AccountGroupMember.Key.class, 1, 2);
+ checkColumns(AccountGroupMember.class, 1);
+
+ checkColumns(AccountGroupMemberAudit.Key.class, 1, 2, 3);
+ checkColumns(AccountGroupMemberAudit.class, 1, 2, 3, 4);
+ }
+
+ @Singleton
+ public static class Factory {
+ private final AuditLogReader auditLogReader;
+
+ @Inject
+ Factory(AuditLogReader auditLogReader) {
+ this.auditLogReader = auditLogReader;
+ }
+
+ public GroupBundle fromReviewDb(ReviewDb db, AccountGroup.Id id) throws OrmException {
+ AccountGroup group = db.accountGroups().get(id);
+ if (group == null) {
+ throw new OrmException("Group " + id + " not found");
+ }
+ return create(
+ group,
+ db.accountGroupMembers().byGroup(id),
+ db.accountGroupMembersAudit().byGroup(id),
+ db.accountGroupById().byGroup(id),
+ db.accountGroupByIdAud().byGroup(id));
+ }
+
+ public GroupBundle fromNoteDb(Repository repo, AccountGroup.UUID uuid)
+ throws ConfigInvalidException, IOException {
+ GroupConfig groupConfig = GroupConfig.loadForGroup(repo, uuid);
+ InternalGroup internalGroup = groupConfig.getLoadedGroup().get();
+ AccountGroup.Id groupId = internalGroup.getId();
+
+ AccountGroup accountGroup =
+ new AccountGroup(
+ internalGroup.getNameKey(),
+ internalGroup.getId(),
+ internalGroup.getGroupUUID(),
+ internalGroup.getCreatedOn());
+ accountGroup.setDescription(internalGroup.getDescription());
+ accountGroup.setOwnerGroupUUID(internalGroup.getOwnerGroupUUID());
+ accountGroup.setVisibleToAll(internalGroup.isVisibleToAll());
+
+ return create(
+ accountGroup,
+ internalGroup
+ .getMembers()
+ .stream()
+ .map(
+ accountId ->
+ new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId)))
+ .collect(toImmutableSet()),
+ auditLogReader.getMembersAudit(repo, uuid),
+ internalGroup
+ .getSubgroups()
+ .stream()
+ .map(
+ subgroupUuid ->
+ new AccountGroupById(new AccountGroupById.Key(groupId, subgroupUuid)))
+ .collect(toImmutableSet()),
+ auditLogReader.getSubgroupsAudit(repo, uuid));
+ }
+ }
+
+ public static GroupBundle create(
+ AccountGroup group,
+ Iterable<AccountGroupMember> members,
+ Iterable<AccountGroupMemberAudit> memberAudit,
+ Iterable<AccountGroupById> byId,
+ Iterable<AccountGroupByIdAud> byIdAudit) {
+ return new AutoValue_GroupBundle.Builder()
+ .group(group)
+ .members(members)
+ .memberAudit(memberAudit)
+ .byId(byId)
+ .byIdAudit(byIdAudit)
+ .build();
+ }
+
+ static Builder builder() {
+ return new AutoValue_GroupBundle.Builder().members().memberAudit().byId().byIdAudit();
+ }
+
+ public AccountGroup.Id id() {
+ return group().getId();
+ }
+
+ public AccountGroup.UUID uuid() {
+ return group().getGroupUUID();
+ }
+
+ public abstract AccountGroup group();
+
+ public abstract ImmutableSet<AccountGroupMember> members();
+
+ public abstract ImmutableSet<AccountGroupMemberAudit> memberAudit();
+
+ public abstract ImmutableSet<AccountGroupById> byId();
+
+ public abstract ImmutableSet<AccountGroupByIdAud> byIdAudit();
+
+ public abstract Builder toBuilder();
+
+ public GroupBundle truncateToSecond() {
+ AccountGroup newGroup = new AccountGroup(group());
+ if (newGroup.getCreatedOn() != null) {
+ newGroup.setCreatedOn(TimeUtil.truncateToSecond(newGroup.getCreatedOn()));
+ }
+ return toBuilder()
+ .group(newGroup)
+ .memberAudit(
+ memberAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet()))
+ .byIdAudit(
+ byIdAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet()))
+ .build();
+ }
+
+ private static AccountGroupMemberAudit truncateToSecond(AccountGroupMemberAudit a) {
+ AccountGroupMemberAudit result =
+ new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(
+ a.getKey().getParentKey(),
+ a.getKey().getGroupId(),
+ TimeUtil.truncateToSecond(a.getKey().getAddedOn())),
+ a.getAddedBy());
+ if (a.getRemovedOn() != null) {
+ result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn()));
+ }
+ return result;
+ }
+
+ private static AccountGroupByIdAud truncateToSecond(AccountGroupByIdAud a) {
+ AccountGroupByIdAud result =
+ new AccountGroupByIdAud(
+ new AccountGroupByIdAud.Key(
+ a.getKey().getParentKey(),
+ a.getKey().getIncludeUUID(),
+ TimeUtil.truncateToSecond(a.getKey().getAddedOn())),
+ a.getAddedBy());
+ if (a.getRemovedOn() != null) {
+ result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn()));
+ }
+ return result;
+ }
+
+ public InternalGroup toInternalGroup() {
+ return InternalGroup.create(
+ group(),
+ members().stream().map(AccountGroupMember::getAccountId).collect(toImmutableSet()),
+ byId().stream().map(AccountGroupById::getIncludeUUID).collect(toImmutableSet()));
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder group(AccountGroup group);
+
+ abstract Builder members(AccountGroupMember... member);
+
+ abstract Builder members(Iterable<AccountGroupMember> member);
+
+ abstract Builder memberAudit(AccountGroupMemberAudit... audit);
+
+ abstract Builder memberAudit(Iterable<AccountGroupMemberAudit> audit);
+
+ abstract Builder byId(AccountGroupById... byId);
+
+ abstract Builder byId(Iterable<AccountGroupById> byId);
+
+ abstract Builder byIdAudit(AccountGroupByIdAud... audit);
+
+ abstract Builder byIdAudit(Iterable<AccountGroupByIdAud> audit);
+
+ abstract GroupBundle build();
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 6c57b67..c23dc09 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -25,6 +25,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -40,14 +41,22 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
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.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
// TODO(aliceks): Add Javadoc descriptions to this file.
public class GroupConfig extends VersionedMetaData {
- private static final String GROUP_CONFIG_FILE = "group.config";
+ public static final String GROUP_CONFIG_FILE = "group.config";
+
+ static final FooterKey FOOTER_ADD_MEMBER = new FooterKey("Add");
+ static final FooterKey FOOTER_REMOVE_MEMBER = new FooterKey("Remove");
+ static final FooterKey FOOTER_ADD_GROUP = new FooterKey("Add-group");
+ static final FooterKey FOOTER_REMOVE_GROUP = new FooterKey("Remove-group");
+
private static final String MEMBERS_FILE = "members";
private static final String SUBGROUPS_FILE = "subgroups";
private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R");
@@ -88,8 +97,7 @@
return loadedGroup;
}
- private void setGroupCreation(InternalGroupCreation groupCreation)
- throws OrmDuplicateKeyException {
+ void setGroupCreation(InternalGroupCreation groupCreation) throws OrmDuplicateKeyException {
checkLoaded();
if (loadedGroup.isPresent()) {
throw new OrmDuplicateKeyException(String.format("Group %s already exists", groupUuid.get()));
@@ -124,7 +132,9 @@
Config config = readConfig(GROUP_CONFIG_FILE);
ImmutableSet<Account.Id> members = readMembers();
ImmutableSet<AccountGroup.UUID> subgroups = readSubgroups();
- loadedGroup = Optional.of(createFrom(groupUuid, config, members, subgroups, createdOn));
+ loadedGroup =
+ Optional.of(
+ createFrom(groupUuid, config, members, subgroups, createdOn, revision.toObjectId()));
}
isLoaded = true;
@@ -135,7 +145,8 @@
Config config,
ImmutableSet<Account.Id> members,
ImmutableSet<AccountGroup.UUID> subgroups,
- Timestamp createdOn) {
+ Timestamp createdOn,
+ ObjectId refState) {
InternalGroup.Builder group = InternalGroup.builder();
group.setGroupUUID(groupUuid);
Arrays.stream(GroupConfigEntry.values())
@@ -143,10 +154,18 @@
group.setMembers(members);
group.setSubgroups(subgroups);
group.setCreatedOn(createdOn);
+ group.setRefState(refState);
return group.build();
}
@Override
+ public RevCommit commit(MetaDataUpdate update) throws IOException {
+ RevCommit c = super.commit(update);
+ loadedGroup = Optional.of(loadedGroup.get().toBuilder().setRefState(c.toObjectId()).build());
+ return c;
+ }
+
+ @Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
checkLoaded();
if (!groupCreation.isPresent() && !groupUpdate.isPresent()) {
@@ -185,7 +204,8 @@
config,
updatedMembers.orElse(originalMembers),
updatedSubgroups.orElse(originalSubgroups),
- createdOn));
+ createdOn,
+ null));
groupCreation = Optional.empty();
return true;
@@ -319,12 +339,12 @@
Sets.difference(oldMembers, newMembers)
.stream()
.map(accountNameEmailRetriever)
- .map("Remove: "::concat);
+ .map((FOOTER_REMOVE_MEMBER.getName() + ": ")::concat);
Stream<String> addedMembers =
Sets.difference(newMembers, oldMembers)
.stream()
.map(accountNameEmailRetriever)
- .map("Add: "::concat);
+ .map((FOOTER_ADD_MEMBER.getName() + ": ")::concat);
return Stream.concat(removedMembers, addedMembers);
}
@@ -334,12 +354,12 @@
Sets.difference(oldSubgroups, newSubgroups)
.stream()
.map(groupNameRetriever)
- .map("Remove-group: "::concat);
+ .map((FOOTER_REMOVE_GROUP.getName() + ": ")::concat);
Stream<String> addedMembers =
Sets.difference(newSubgroups, oldSubgroups)
.stream()
.map(groupNameRetriever)
- .map("Add-group: "::concat);
+ .map((FOOTER_ADD_GROUP.getName() + ": ")::concat);
return Stream.concat(removedMembers, addedMembers);
}
}
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
index 025b0e9..0daceb3 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
@@ -32,7 +32,11 @@
@Override
void initNewConfig(Config config, InternalGroupCreation group) {
AccountGroup.Id id = group.getId();
- config.setInt(SECTION_NAME, null, super.keyName, id.get());
+
+ // Do not use config.setInt(...) to write the group ID because config.setInt(...) persists
+ // integers that can be expressed in KiB as a unit strings, e.g. "1024" is stored as "1k".
+ // Using config.setString(...) ensures that group IDs are human readable.
+ config.setString(SECTION_NAME, null, super.keyName, Integer.toString(id.get()));
}
@Override
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
new file mode 100644
index 0000000..ee7f849
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -0,0 +1,312 @@
+// 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.group.db;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableBiMap.toImmutableBiMap;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+// TODO(aliceks): Add Javadoc descriptions.
+public class GroupNameNotes extends VersionedMetaData {
+ private static final String SECTION_NAME = "group";
+ private static final String UUID_PARAM = "uuid";
+ private static final String NAME_PARAM = "name";
+
+ @VisibleForTesting
+ static final String UNIQUE_REF_ERROR = "GroupReference collection must contain unique references";
+
+ public static void updateGroupNames(
+ Repository allUsersRepo,
+ ObjectInserter inserter,
+ BatchRefUpdate bru,
+ Collection<GroupReference> groupReferences,
+ PersonIdent ident)
+ throws IOException {
+ // Not strictly necessary for iteration; throws IAE if it encounters duplicates, which is nice.
+ ImmutableBiMap<AccountGroup.UUID, String> biMap = toBiMap(groupReferences);
+
+ try (ObjectReader reader = inserter.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ // Always start from an empty map, discarding old notes.
+ NoteMap noteMap = NoteMap.newEmptyMap();
+ Ref ref = allUsersRepo.exactRef(RefNames.REFS_GROUPNAMES);
+ RevCommit oldCommit = ref != null ? rw.parseCommit(ref.getObjectId()) : null;
+
+ for (Map.Entry<AccountGroup.UUID, String> e : biMap.entrySet()) {
+ AccountGroup.NameKey nameKey = new AccountGroup.NameKey(e.getValue());
+ ObjectId noteKey = getNoteKey(nameKey);
+ noteMap.set(noteKey, getAsNoteData(e.getKey(), nameKey), inserter);
+ }
+
+ ObjectId newTreeId = noteMap.writeTree(inserter);
+ if (oldCommit != null && newTreeId.equals(oldCommit.getTree())) {
+ return;
+ }
+ CommitBuilder cb = new CommitBuilder();
+ if (oldCommit != null) {
+ cb.addParentId(oldCommit);
+ }
+ cb.setTreeId(newTreeId);
+ cb.setAuthor(ident);
+ cb.setCommitter(ident);
+ int n = groupReferences.size();
+ cb.setMessage("Store " + n + " group name" + (n != 1 ? "s" : ""));
+ ObjectId newId = inserter.insert(cb).copy();
+
+ ObjectId oldId = oldCommit != null ? oldCommit.copy() : ObjectId.zeroId();
+ bru.addCommand(new ReceiveCommand(oldId, newId, RefNames.REFS_GROUPNAMES));
+ }
+ }
+
+ private static ImmutableBiMap<AccountGroup.UUID, String> toBiMap(
+ Collection<GroupReference> groupReferences) {
+ try {
+ return groupReferences
+ .stream()
+ .collect(toImmutableBiMap(gr -> gr.getUUID(), gr -> gr.getName()));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(UNIQUE_REF_ERROR, e);
+ }
+ }
+
+ private final AccountGroup.UUID groupUuid;
+ private final Optional<AccountGroup.NameKey> oldGroupName;
+ private final Optional<AccountGroup.NameKey> newGroupName;
+
+ private boolean nameConflicting;
+
+ private GroupNameNotes(
+ AccountGroup.UUID groupUuid,
+ @Nullable AccountGroup.NameKey oldGroupName,
+ @Nullable AccountGroup.NameKey newGroupName) {
+ this.groupUuid = checkNotNull(groupUuid);
+
+ if (Objects.equals(oldGroupName, newGroupName)) {
+ this.oldGroupName = Optional.empty();
+ this.newGroupName = Optional.empty();
+ } else {
+ this.oldGroupName = Optional.ofNullable(oldGroupName);
+ this.newGroupName = Optional.ofNullable(newGroupName);
+ }
+ }
+
+ public static GroupNameNotes loadForRename(
+ Repository repository,
+ AccountGroup.UUID groupUuid,
+ AccountGroup.NameKey oldName,
+ AccountGroup.NameKey newName)
+ throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+ checkNotNull(oldName);
+ checkNotNull(newName);
+
+ GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, oldName, newName);
+ groupNameNotes.load(repository);
+ groupNameNotes.ensureNewNameIsNotUsed();
+ return groupNameNotes;
+ }
+
+ public static GroupNameNotes loadForNewGroup(
+ Repository repository, AccountGroup.UUID groupUuid, AccountGroup.NameKey groupName)
+ throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+ checkNotNull(groupName);
+
+ GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, null, groupName);
+ groupNameNotes.load(repository);
+ groupNameNotes.ensureNewNameIsNotUsed();
+ return groupNameNotes;
+ }
+
+ public static ImmutableSet<GroupReference> loadAllGroupReferences(Repository repository)
+ throws IOException, ConfigInvalidException {
+ Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
+ if (ref == null) {
+ return ImmutableSet.of();
+ }
+ try (RevWalk revWalk = new RevWalk(repository);
+ ObjectReader reader = revWalk.getObjectReader()) {
+ RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
+ NoteMap noteMap = NoteMap.read(reader, notesCommit);
+ ImmutableSet.Builder<GroupReference> groupReferences = ImmutableSet.builder();
+ for (Note note : noteMap) {
+ GroupReference groupReference = getGroupReference(reader, note.getData());
+ groupReferences.add(groupReference);
+ }
+ return groupReferences.build();
+ }
+ }
+
+ @Override
+ protected String getRefName() {
+ return RefNames.REFS_GROUPNAMES;
+ }
+
+ @Override
+ protected void onLoad() throws IOException, ConfigInvalidException {
+ nameConflicting = false;
+
+ if (revision != null) {
+ NoteMap noteMap = NoteMap.read(reader, revision);
+ if (newGroupName.isPresent()) {
+ ObjectId newNameId = getNoteKey(newGroupName.get());
+ nameConflicting = noteMap.contains(newNameId);
+ }
+ ensureOldNameIsPresent(noteMap);
+ }
+ }
+
+ private void ensureOldNameIsPresent(NoteMap noteMap) throws IOException, ConfigInvalidException {
+ if (oldGroupName.isPresent()) {
+ AccountGroup.NameKey oldName = oldGroupName.get();
+ ObjectId noteKey = getNoteKey(oldName);
+ ObjectId noteDataBlobId = noteMap.get(noteKey);
+ if (noteDataBlobId == null) {
+ throw new ConfigInvalidException(
+ String.format("Group name '%s' doesn't exist in the list of all names", oldName));
+ }
+ GroupReference group = getGroupReference(reader, noteDataBlobId);
+ AccountGroup.UUID foundUuid = group.getUUID();
+ if (!Objects.equals(groupUuid, foundUuid)) {
+ throw new ConfigInvalidException(
+ String.format(
+ "Name '%s' points to UUID '%s' and not to '%s'", oldName, foundUuid, groupUuid));
+ }
+ }
+ }
+
+ private void ensureNewNameIsNotUsed() throws OrmDuplicateKeyException {
+ if (newGroupName.isPresent() && nameConflicting) {
+ throw new OrmDuplicateKeyException(
+ String.format("Name '%s' is already used", newGroupName.get().get()));
+ }
+ }
+
+ @Override
+ protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
+ if (!oldGroupName.isPresent() && !newGroupName.isPresent()) {
+ return false;
+ }
+
+ NoteMap noteMap = revision == null ? NoteMap.newEmptyMap() : NoteMap.read(reader, revision);
+ if (oldGroupName.isPresent()) {
+ removeNote(noteMap, oldGroupName.get(), inserter);
+ }
+
+ if (newGroupName.isPresent()) {
+ addNote(noteMap, newGroupName.get(), groupUuid, inserter);
+ }
+
+ commit.setTreeId(noteMap.writeTree(inserter));
+ commit.setMessage(getCommitMessage());
+
+ return true;
+ }
+
+ private static void removeNote(
+ NoteMap noteMap, AccountGroup.NameKey groupName, ObjectInserter inserter) throws IOException {
+ ObjectId noteKey = getNoteKey(groupName);
+ noteMap.set(noteKey, null, inserter);
+ }
+
+ private static void addNote(
+ NoteMap noteMap,
+ AccountGroup.NameKey groupName,
+ AccountGroup.UUID groupUuid,
+ ObjectInserter inserter)
+ throws IOException {
+ ObjectId noteKey = getNoteKey(groupName);
+ noteMap.set(noteKey, getAsNoteData(groupUuid, groupName), inserter);
+ }
+
+ // Use the same approach as ExternalId.Key.sha1().
+ @SuppressWarnings("deprecation")
+ @VisibleForTesting
+ static ObjectId getNoteKey(AccountGroup.NameKey groupName) {
+ return ObjectId.fromRaw(Hashing.sha1().hashString(groupName.get(), UTF_8).asBytes());
+ }
+
+ private static String getAsNoteData(AccountGroup.UUID uuid, AccountGroup.NameKey groupName) {
+ Config config = new Config();
+ config.setString(SECTION_NAME, null, UUID_PARAM, uuid.get());
+ config.setString(SECTION_NAME, null, NAME_PARAM, groupName.get());
+ return config.toText();
+ }
+
+ @VisibleForTesting
+ public static GroupReference getGroupReference(ObjectReader reader, ObjectId noteDataBlobId)
+ throws IOException, ConfigInvalidException {
+ byte[] noteData = reader.open(noteDataBlobId, OBJ_BLOB).getCachedBytes();
+ return getFromNoteData(noteData);
+ }
+
+ private static GroupReference getFromNoteData(byte[] noteData) throws ConfigInvalidException {
+ Config config = new Config();
+ config.fromText(new String(noteData, UTF_8));
+
+ String uuid = config.getString(SECTION_NAME, null, UUID_PARAM);
+ String name = config.getString(SECTION_NAME, null, NAME_PARAM);
+ if (uuid == null || name == null) {
+ throw new ConfigInvalidException(
+ String.format("UUID '%s' and name '%s' must be defined", uuid, name));
+ }
+
+ return new GroupReference(new AccountGroup.UUID(uuid), name);
+ }
+
+ private String getCommitMessage() {
+ if (oldGroupName.isPresent() && newGroupName.isPresent()) {
+ return String.format(
+ "Rename group from '%s' to '%s'", oldGroupName.get(), newGroupName.get());
+ }
+ if (newGroupName.isPresent()) {
+ return String.format("Create group '%s'", newGroupName.get());
+ }
+ if (oldGroupName.isPresent()) {
+ return String.format("Delete group '%s'", oldGroupName.get());
+ }
+ return "No-op";
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/GroupRebuilder.java b/java/com/google/gerrit/server/group/db/GroupRebuilder.java
new file mode 100644
index 0000000..7f8d8a9
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/GroupRebuilder.java
@@ -0,0 +1,331 @@
+// 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.group.db;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Sets;
+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.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gerrit.server.group.db.InternalGroupUpdate.MemberModification;
+import com.google.gerrit.server.group.db.InternalGroupUpdate.SubgroupModification;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Helper for rebuilding an entire group's NoteDb refs. */
+@Singleton
+public class GroupRebuilder {
+ private final Provider<PersonIdent> serverIdent;
+ private final AllUsersName allUsers;
+ private final MetaDataUpdate.InternalFactory metaDataUpdateFactory;
+
+ private final BiFunction<Account.Id, PersonIdent, PersonIdent> newPersonIdentFunc;
+ private final Function<Account.Id, String> getAccountNameEmailFunc;
+ private final Function<AccountGroup.UUID, String> getGroupNameFunc;
+
+ @Inject
+ GroupRebuilder(
+ @AnonymousCowardName String anonymousCowardName,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent,
+ @GerritServerId String serverId,
+ AllUsersName allUsers,
+ MetaDataUpdate.InternalFactory metaDataUpdateFactory,
+ AccountCache accountCache,
+ GroupCache groupCache) {
+ this(
+ serverIdent,
+ allUsers,
+ metaDataUpdateFactory,
+
+ // TODO(dborowitz): These probably won't work during init.
+ (id, ident) ->
+ new PersonIdent(
+ GroupsUpdate.getAccountName(accountCache, anonymousCowardName, id),
+ GroupsUpdate.getEmailForAuditLog(id, serverId),
+ ident.getWhen(),
+ ident.getTimeZone()),
+ id -> GroupsUpdate.getAccountNameEmail(accountCache, anonymousCowardName, id, serverId),
+ uuid -> GroupsUpdate.getGroupName(groupCache, uuid));
+ }
+
+ @VisibleForTesting
+ GroupRebuilder(
+ Provider<PersonIdent> serverIdent,
+ AllUsersName allUsers,
+ MetaDataUpdate.InternalFactory metaDataUpdateFactory,
+ BiFunction<Account.Id, PersonIdent, PersonIdent> newPersonIdentFunc,
+ Function<Account.Id, String> getAccountNameEmailFunc,
+ Function<AccountGroup.UUID, String> getGroupNameFunc) {
+ this.serverIdent = serverIdent;
+ this.allUsers = allUsers;
+ this.metaDataUpdateFactory = metaDataUpdateFactory;
+ this.newPersonIdentFunc = newPersonIdentFunc;
+ this.getAccountNameEmailFunc = getAccountNameEmailFunc;
+ this.getGroupNameFunc = getGroupNameFunc;
+ }
+
+ public void rebuild(Repository allUsersRepo, GroupBundle bundle, @Nullable BatchRefUpdate bru)
+ throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
+ GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, bundle.uuid());
+ AccountGroup group = bundle.group();
+ groupConfig.setGroupCreation(
+ InternalGroupCreation.builder()
+ .setId(bundle.id())
+ .setNameKey(group.getNameKey())
+ .setGroupUUID(group.getGroupUUID())
+ .setCreatedOn(group.getCreatedOn())
+ .build());
+
+ InternalGroupUpdate.Builder updateBuilder =
+ InternalGroupUpdate.builder()
+ .setOwnerGroupUUID(group.getOwnerGroupUUID())
+ .setVisibleToAll(group.isVisibleToAll());
+ if (bundle.group().getDescription() != null) {
+ updateBuilder.setDescription(group.getDescription());
+ }
+ groupConfig.setGroupUpdate(updateBuilder.build(), getAccountNameEmailFunc, getGroupNameFunc);
+
+ Map<Key, Collection<Event>> events = toEvents(bundle).asMap();
+ PersonIdent nowServerIdent = getServerIdent(events);
+
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers, allUsersRepo, bru);
+
+ // Creation is done by the server (unlike later audit events).
+ PersonIdent created = new PersonIdent(nowServerIdent, group.getCreatedOn());
+ md.getCommitBuilder().setAuthor(created);
+ md.getCommitBuilder().setCommitter(created);
+
+ // Rebuild group ref.
+ try (BatchMetaDataUpdate batch = groupConfig.openUpdate(md)) {
+ batch.write(groupConfig, md.getCommitBuilder());
+
+ for (Map.Entry<Key, Collection<Event>> e : events.entrySet()) {
+ InternalGroupUpdate.Builder ub = InternalGroupUpdate.builder();
+ e.getValue().forEach(event -> event.update().accept(ub));
+ groupConfig.setGroupUpdate(ub.build(), getAccountNameEmailFunc, getGroupNameFunc);
+
+ PersonIdent currServerIdent = new PersonIdent(nowServerIdent, e.getKey().when());
+ CommitBuilder cb = new CommitBuilder();
+ cb.setAuthor(
+ e.getKey()
+ .accountId()
+ .map(id -> newPersonIdentFunc.apply(id, currServerIdent))
+ .orElse(currServerIdent));
+ cb.setCommitter(currServerIdent);
+ batch.write(groupConfig, cb);
+ }
+
+ batch.createRef(groupConfig.getRefName());
+ }
+ }
+
+ private ListMultimap<Key, Event> toEvents(GroupBundle bundle) {
+ ListMultimap<Key, Event> result =
+ MultimapBuilder.treeKeys(Key.COMPARATOR).arrayListValues(1).build();
+ Event e;
+
+ for (AccountGroupMemberAudit a : bundle.memberAudit()) {
+ checkArgument(
+ a.getKey().getGroupId().equals(bundle.id()),
+ "key %s does not match group %s",
+ a.getKey(),
+ bundle.id());
+ Account.Id accountId = a.getKey().getParentKey();
+ e = event(Type.ADD_MEMBER, a.getAddedBy(), a.getKey().getAddedOn(), addMember(accountId));
+ result.put(e.key(), e);
+ if (!a.isActive()) {
+ e = event(Type.REMOVE_MEMBER, a.getRemovedBy(), a.getRemovedOn(), removeMember(accountId));
+ result.put(e.key(), e);
+ }
+ }
+
+ for (AccountGroupByIdAud a : bundle.byIdAudit()) {
+ checkArgument(
+ a.getKey().getParentKey().equals(bundle.id()),
+ "key %s does not match group %s",
+ a.getKey(),
+ bundle.id());
+ AccountGroup.UUID uuid = a.getKey().getIncludeUUID();
+ e = event(Type.ADD_GROUP, a.getAddedBy(), a.getKey().getAddedOn(), addGroup(uuid));
+ result.put(e.key(), e);
+ if (!a.isActive()) {
+ e = event(Type.REMOVE_GROUP, a.getRemovedBy(), a.getRemovedOn(), removeGroup(uuid));
+ result.put(e.key(), e);
+ }
+ }
+
+ // Due to clock skew, audit events may be in the future relative to this machine. Ensure the
+ // fixup event happens after any other events, both for the purposes of sorting Keys correctly
+ // and to avoid non-monotonic timestamps in the commit history.
+ Timestamp maxTs =
+ Stream.concat(result.keySet().stream().map(Key::when), Stream.of(TimeUtil.nowTs()))
+ .max(Comparator.naturalOrder())
+ .get();
+ Timestamp fixupTs = new Timestamp(maxTs.getTime() + 1);
+ e = serverEvent(Type.FIXUP, fixupTs, setCurrentMembership(bundle));
+ result.put(e.key(), e);
+
+ return result;
+ }
+
+ private PersonIdent getServerIdent(Map<Key, Collection<Event>> events) {
+ // Created with MultimapBuilder.treeKeys, so the keySet is navigable.
+ Key lastKey = ((NavigableSet<Key>) events.keySet()).last();
+ checkState(lastKey.type() == Type.FIXUP);
+ PersonIdent ident = serverIdent.get();
+ return new PersonIdent(
+ ident.getName(),
+ ident.getEmailAddress(),
+ Iterables.getOnlyElement(events.get(lastKey)).when(),
+ ident.getTimeZone());
+ }
+
+ private static Consumer<InternalGroupUpdate.Builder> addMember(Account.Id toAdd) {
+ return b -> {
+ MemberModification prev = b.getMemberModification();
+ b.setMemberModification(in -> Sets.union(prev.apply(in), ImmutableSet.of(toAdd)));
+ };
+ }
+
+ private static Consumer<InternalGroupUpdate.Builder> removeMember(Account.Id toRemove) {
+ return b -> {
+ MemberModification prev = b.getMemberModification();
+ b.setMemberModification(in -> Sets.difference(prev.apply(in), ImmutableSet.of(toRemove)));
+ };
+ }
+
+ private static Consumer<InternalGroupUpdate.Builder> addGroup(AccountGroup.UUID toAdd) {
+ return b -> {
+ SubgroupModification prev = b.getSubgroupModification();
+ b.setSubgroupModification(in -> Sets.union(prev.apply(in), ImmutableSet.of(toAdd)));
+ };
+ }
+
+ private static Consumer<InternalGroupUpdate.Builder> removeGroup(AccountGroup.UUID toRemove) {
+ return b -> {
+ SubgroupModification prev = b.getSubgroupModification();
+ b.setSubgroupModification(in -> Sets.difference(prev.apply(in), ImmutableSet.of(toRemove)));
+ };
+ }
+
+ private static Consumer<InternalGroupUpdate.Builder> setCurrentMembership(GroupBundle bundle) {
+ // Overwrite members and subgroups with the current values. The storage layer will do the
+ // set differences to compute the appropriate delta, if any.
+ return b ->
+ b.setMemberModification(
+ in ->
+ bundle.members().stream().map(m -> m.getAccountId()).collect(toImmutableSet()))
+ .setSubgroupModification(
+ in ->
+ bundle.byId().stream().map(m -> m.getIncludeUUID()).collect(toImmutableSet()));
+ }
+
+ private static Event event(
+ Type type,
+ Account.Id accountId,
+ Timestamp when,
+ Consumer<InternalGroupUpdate.Builder> update) {
+ return new AutoValue_GroupRebuilder_Event(type, Optional.of(accountId), when, update);
+ }
+
+ private static Event serverEvent(
+ Type type, Timestamp when, Consumer<InternalGroupUpdate.Builder> update) {
+ return new AutoValue_GroupRebuilder_Event(type, Optional.empty(), when, update);
+ }
+
+ @AutoValue
+ abstract static class Event {
+ abstract Type type();
+
+ abstract Optional<Account.Id> accountId();
+
+ abstract Timestamp when();
+
+ abstract Consumer<InternalGroupUpdate.Builder> update();
+
+ Key key() {
+ return new AutoValue_GroupRebuilder_Key(accountId(), when(), type());
+ }
+ }
+
+ /**
+ * Distinct event types.
+ *
+ * <p>Events at the same time by the same user are batched together by type. The types should
+ * correspond to the possible batch operations supported by {@link
+ * com.google.gerrit.server.audit.AuditService}.
+ */
+ enum Type {
+ ADD_MEMBER,
+ REMOVE_MEMBER,
+ ADD_GROUP,
+ REMOVE_GROUP,
+ FIXUP;
+ }
+
+ @AutoValue
+ abstract static class Key {
+ static final Comparator<Key> COMPARATOR =
+ Comparator.comparing(Key::when)
+ .thenComparing(
+ k -> k.accountId().map(Account.Id::get).orElse(null),
+ Comparator.nullsFirst(Comparator.naturalOrder()))
+ .thenComparing(Key::type);
+
+ abstract Optional<Account.Id> accountId();
+
+ abstract Timestamp when();
+
+ abstract Type type();
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index 2b1cb3d..ba60fbc 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -29,9 +30,9 @@
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
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.group.InternalGroup;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
@@ -42,10 +43,7 @@
import java.util.Optional;
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;
/**
* A database accessor for read calls related to groups.
@@ -55,24 +53,28 @@
* directly. There are a few exceptions though: schema classes, wrapper classes, and classes
* executed during init. The latter ones should use {@code GroupsOnInit} instead.
*
+ * <p>Most callers should not need to read groups directly from the database; they should use the
+ * {@link com.google.gerrit.server.account.GroupCache GroupCache} instead.
+ *
* <p>If not explicitly stated, all methods of this class refer to <em>internal</em> groups.
*/
@Singleton
public class Groups {
- private static final Logger log = LoggerFactory.getLogger(Groups.class);
-
- private final boolean readFromNoteDb;
+ private final GroupsMigration groupsMigration;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
+ private final AuditLogReader auditLogReader;
@Inject
public Groups(
- @GerritServerConfig Config config,
+ GroupsMigration groupsMigration,
GitRepositoryManager repoManager,
- AllUsersName allUsersName) {
- readFromNoteDb = config.getBoolean("user", null, "readGroupsFromNoteDb", false);
+ AllUsersName allUsersName,
+ AuditLogReader auditLogReader) {
+ this.groupsMigration = groupsMigration;
this.repoManager = repoManager;
this.allUsersName = allUsersName;
+ this.auditLogReader = auditLogReader;
}
/**
@@ -105,7 +107,7 @@
*/
public Optional<InternalGroup> getGroup(ReviewDb db, AccountGroup.UUID groupUuid)
throws OrmException, IOException, ConfigInvalidException {
- if (readFromNoteDb) {
+ if (groupsMigration.readFromNoteDb()) {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
return getGroupFromNoteDb(allUsersRepo, groupUuid);
}
@@ -125,7 +127,7 @@
return groupConfig.getLoadedGroup();
}
- private static InternalGroup asInternalGroup(ReviewDb db, AccountGroup accountGroup)
+ public static InternalGroup asInternalGroup(ReviewDb db, AccountGroup accountGroup)
throws OrmException {
ImmutableSet<Account.Id> members =
getMembersFromReviewDb(db, accountGroup.getId()).collect(toImmutableSet());
@@ -171,25 +173,25 @@
}
}
- public Stream<InternalGroup> getAll(ReviewDb db) throws OrmException {
- // TODO(aliceks): Add code for NoteDb.
- return getAllUuids(db)
- .map(groupUuid -> getGroupIfPossible(db, groupUuid))
- .flatMap(Streams::stream);
- }
-
- public Stream<AccountGroup.UUID> getAllUuids(ReviewDb db) throws OrmException {
- // TODO(aliceks): Add code for NoteDb.
- return Streams.stream(db.accountGroups().all()).map(AccountGroup::getGroupUUID);
- }
-
- private Optional<InternalGroup> getGroupIfPossible(ReviewDb db, AccountGroup.UUID groupUuid) {
- try {
- return getGroup(db, groupUuid);
- } catch (OrmException | IOException | ConfigInvalidException e) {
- log.warn(String.format("Cannot look up group %s by uuid", groupUuid.get()), e);
+ /**
+ * Returns {@code GroupReference}s for all internal groups.
+ *
+ * @param db the {@code ReviewDb} instance to use for lookups
+ * @return a stream of the {@code GroupReference}s of all internal groups
+ * @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
+ */
+ public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+ return GroupNameNotes.loadAllGroupReferences(allUsersRepo).stream();
+ }
}
- return Optional.empty();
+
+ return Streams.stream(db.accountGroups().all())
+ .map(group -> new GroupReference(group.getGroupUUID(), group.getName()));
}
/**
@@ -273,33 +275,60 @@
* @param db the {@code ReviewDb} instance to use for lookups
* @return a stream of the UUIDs of the known external groups
* @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
*/
- public Stream<AccountGroup.UUID> getExternalGroups(ReviewDb db) throws OrmException {
- // TODO(aliceks): Add code for NoteDb.
+ public Stream<AccountGroup.UUID> getExternalGroups(ReviewDb db)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+ return getExternalGroupsFromNoteDb(allUsersRepo);
+ }
+ }
+
return Streams.stream(db.accountGroupById().all())
.map(AccountGroupById::getIncludeUUID)
.distinct()
.filter(groupUuid -> !AccountGroup.isInternalGroup(groupUuid));
}
+ private Stream<AccountGroup.UUID> getExternalGroupsFromNoteDb(Repository allUsersRepo)
+ throws IOException, ConfigInvalidException {
+ ImmutableSet<GroupReference> allInternalGroups =
+ GroupNameNotes.loadAllGroupReferences(allUsersRepo);
+ ImmutableSet.Builder<AccountGroup.UUID> allSubgroups = ImmutableSet.builder();
+ for (GroupReference internalGroup : allInternalGroups) {
+ Optional<InternalGroup> group = getGroupFromNoteDb(allUsersRepo, internalGroup.getUUID());
+ group.map(InternalGroup::getSubgroups).ifPresent(allSubgroups::addAll);
+ }
+ return allSubgroups
+ .build()
+ .stream()
+ .filter(groupUuid -> !AccountGroup.isInternalGroup(groupUuid));
+ }
+
/**
* Returns the membership audit records for a given group.
*
* @param db the {@code ReviewDb} instance to use for lookups
+ * @param repo All-Users repository.
* @param groupUuid the UUID of the group
* @return the audit records, in arbitrary order; empty if the group does not exist
* @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
*/
- public List<AccountGroupMemberAudit> getMembersAudit(ReviewDb db, AccountGroup.UUID groupUuid)
- throws OrmException {
- if (readFromNoteDb) {
- // TODO(dborowitz): Implement.
- throw new OrmException("Audit logs not yet implemented in NoteDb");
+ public List<AccountGroupMemberAudit> getMembersAudit(
+ ReviewDb db, Repository repo, AccountGroup.UUID groupUuid)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ return auditLogReader.getMembersAudit(repo, groupUuid);
}
Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
if (!group.isPresent()) {
return ImmutableList.of();
}
+
return db.accountGroupMembersAudit().byGroup(group.get().getId()).toList();
}
@@ -307,20 +336,24 @@
* Returns the subgroup audit records for a given group.
*
* @param db the {@code ReviewDb} instance to use for lookups
+ * @param repo All-Users repository.
* @param groupUuid the UUID of the group
* @return the audit records, in arbitrary order; empty if the group does not exist
* @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
*/
- public List<AccountGroupByIdAud> getSubgroupsAudit(ReviewDb db, AccountGroup.UUID groupUuid)
- throws OrmException {
- if (readFromNoteDb) {
- // TODO(dborowitz): Implement.
- throw new OrmException("Audit logs not yet implemented in NoteDb");
+ public List<AccountGroupByIdAud> getSubgroupsAudit(
+ ReviewDb db, Repository repo, AccountGroup.UUID groupUuid)
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ return auditLogReader.getSubgroupsAudit(repo, groupUuid);
}
Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
if (!group.isPresent()) {
return ImmutableList.of();
}
+
return db.accountGroupByIdAud().byGroup(group.get().getId()).toList();
}
}
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 81f3d8f..9e3eadb 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -32,6 +32,7 @@
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
@@ -43,10 +44,13 @@
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerId;
+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.git.RenameGroupOp;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.notedb.GroupsMigration;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -58,7 +62,9 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -100,7 +106,9 @@
@Nullable private final IdentifiedUser currentUser;
private final PersonIdent authorIdent;
private final MetaDataUpdateFactory metaDataUpdateFactory;
- private final boolean writeGroupsToNoteDb;
+ private final GroupsMigration groupsMigration;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final boolean reviewDbUpdatesAreBlocked;
@Inject
GroupsUpdate(
@@ -114,9 +122,10 @@
RenameGroupOp.Factory renameGroupOpFactory,
@GerritServerId String serverId,
@GerritPersonIdent PersonIdent serverIdent,
- MetaDataUpdate.User metaDataUpdateUserFactory,
- MetaDataUpdate.Server metaDataUpdateServerFactory,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GroupsMigration groupsMigration,
@GerritServerConfig Config config,
+ GitReferenceUpdated gitRefUpdated,
@Assisted @Nullable IdentifiedUser currentUser) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
@@ -127,44 +136,38 @@
this.anonymousCowardName = anonymousCowardName;
this.renameGroupOpFactory = renameGroupOpFactory;
this.serverId = serverId;
+ this.groupsMigration = groupsMigration;
+ this.gitRefUpdated = gitRefUpdated;
this.currentUser = currentUser;
metaDataUpdateFactory =
getMetaDataUpdateFactory(
- metaDataUpdateUserFactory,
- metaDataUpdateServerFactory,
- currentUser,
- serverIdent,
- serverId,
- anonymousCowardName);
+ metaDataUpdateInternalFactory, currentUser, serverIdent, serverId, anonymousCowardName);
authorIdent = getAuthorIdent(serverIdent, currentUser);
- // TODO(aliceks): Remove this flag when all other necessary TODOs for writing groups to NoteDb
- // have been addressed.
- // Don't flip this flag in a production setting! We only added it to spread the implementation
- // of groups in NoteDb among several changes which are gradually merged.
- writeGroupsToNoteDb = config.getBoolean("user", null, "writeGroupsToNoteDb", false);
+ reviewDbUpdatesAreBlocked = config.getBoolean("user", null, "blockReviewDbGroupUpdates", false);
}
- // TODO(aliceks): Introduce a common class for MetaDataUpdate.User and MetaDataUpdate.Server which
- // doesn't require this ugly code. In addition, allow to pass in the repository and to use another
- // author ident.
private static MetaDataUpdateFactory getMetaDataUpdateFactory(
- MetaDataUpdate.User metaDataUpdateUserFactory,
- MetaDataUpdate.Server metaDataUpdateServerFactory,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
@Nullable IdentifiedUser currentUser,
PersonIdent serverIdent,
String serverId,
String anonymousCowardName) {
- return currentUser != null
- ? projectName -> {
- MetaDataUpdate metaDataUpdate =
- metaDataUpdateUserFactory.create(projectName, currentUser);
- PersonIdent authorIdent =
- getAuditLogAuthorIdent(
- currentUser.getAccount(), serverIdent, serverId, anonymousCowardName);
- metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
- return metaDataUpdate;
- }
- : metaDataUpdateServerFactory::create;
+ return (projectName, repository, batchRefUpdate) -> {
+ MetaDataUpdate metaDataUpdate =
+ metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate);
+ metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
+ PersonIdent authorIdent;
+ if (currentUser != null) {
+ metaDataUpdate.setAuthor(currentUser);
+ authorIdent =
+ getAuditLogAuthorIdent(
+ currentUser.getAccount(), serverIdent, serverId, anonymousCowardName);
+ } else {
+ authorIdent = serverIdent;
+ }
+ metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
+ return metaDataUpdate;
+ };
}
private static PersonIdent getAuditLogAuthorIdent(
@@ -202,13 +205,17 @@
public InternalGroup createGroup(
ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws OrmException, IOException, ConfigInvalidException {
- InternalGroup createdGroupInReviewDb = createGroupInReviewDb(db, groupCreation, groupUpdate);
+ if (!groupsMigration.disableGroupReviewDb()) {
+ InternalGroup createdGroupInReviewDb =
+ createGroupInReviewDb(ReviewDbUtil.unwrapDb(db), groupCreation, groupUpdate);
- if (!writeGroupsToNoteDb) {
- updateCachesOnGroupCreation(createdGroupInReviewDb);
- return createdGroupInReviewDb;
+ if (!groupsMigration.writeToNoteDb()) {
+ updateCachesOnGroupCreation(createdGroupInReviewDb);
+ return createdGroupInReviewDb;
+ }
}
+ // TODO(aliceks): Add retry mechanism.
InternalGroup createdGroup = createGroupInNoteDb(groupCreation, groupUpdate);
updateCachesOnGroupCreation(createdGroup);
return createdGroup;
@@ -237,13 +244,17 @@
public UpdateResult updateGroupInDb(
ReviewDb db, AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
- AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
- UpdateResult reviewDbUpdateResult = updateGroupInReviewDb(db, group, groupUpdate);
+ UpdateResult reviewDbUpdateResult = null;
+ if (!groupsMigration.disableGroupReviewDb()) {
+ AccountGroup group = getExistingGroupFromReviewDb(ReviewDbUtil.unwrapDb(db), groupUuid);
+ reviewDbUpdateResult = updateGroupInReviewDb(ReviewDbUtil.unwrapDb(db), group, groupUpdate);
- if (!writeGroupsToNoteDb) {
- return reviewDbUpdateResult;
+ if (!groupsMigration.writeToNoteDb()) {
+ return reviewDbUpdateResult;
+ }
}
+ // TODO(aliceks): Add retry mechanism.
Optional<UpdateResult> noteDbUpdateResult = updateGroupInNoteDb(groupUuid, groupUpdate);
return noteDbUpdateResult.orElse(reviewDbUpdateResult);
}
@@ -251,6 +262,7 @@
private InternalGroup createGroupInReviewDb(
ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws OrmException {
+ checkIfReviewDbUpdatesAreBlocked();
AccountGroupName gn = new AccountGroupName(groupCreation.getNameKey(), groupCreation.getId());
// first insert the group name to validate that the group name hasn't
@@ -260,7 +272,10 @@
AccountGroup group = createAccountGroup(groupCreation);
UpdateResult updateResult = updateGroupInReviewDb(db, group, groupUpdate);
return InternalGroup.create(
- group, updateResult.getModifiedMembers(), updateResult.getModifiedSubgroups());
+ group,
+ updateResult.getModifiedMembers(),
+ updateResult.getModifiedSubgroups(),
+ updateResult.getRefState());
}
public static AccountGroup createAccountGroup(
@@ -287,6 +302,8 @@
private UpdateResult updateGroupInReviewDb(
ReviewDb db, AccountGroup group, InternalGroupUpdate groupUpdate) throws OrmException {
+ checkIfReviewDbUpdatesAreBlocked();
+
AccountGroup.NameKey originalName = group.getNameKey();
applyUpdate(group, groupUpdate);
AccountGroup.NameKey updatedName = group.getNameKey();
@@ -440,10 +457,14 @@
InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws IOException, ConfigInvalidException, OrmException {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+ AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
+ GroupNameNotes groupNameNotes =
+ GroupNameNotes.loadForNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName);
+
GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersRepo, groupCreation);
- // TODO(aliceks): Find a way to ensure unique names with NoteDb.
groupConfig.setGroupUpdate(groupUpdate, this::getAccountNameEmail, this::getGroupName);
- commit(groupConfig);
+
+ commit(allUsersRepo, groupConfig, groupNameNotes);
return groupConfig
.getLoadedGroup()
@@ -454,19 +475,27 @@
private Optional<UpdateResult> updateGroupInNoteDb(
AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
- throws IOException, ConfigInvalidException {
+ throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, groupUuid);
+ groupConfig.setGroupUpdate(groupUpdate, this::getAccountNameEmail, this::getGroupName);
if (!groupConfig.getLoadedGroup().isPresent()) {
- // TODO(aliceks): Throw a NoSuchGroupException here when all groups are stored in NoteDb.
+ if (groupsMigration.readFromNoteDb()) {
+ throw new NoSuchGroupException(groupUuid);
+ }
return Optional.empty();
}
InternalGroup originalGroup = groupConfig.getLoadedGroup().get();
- // TODO(aliceks): Find a way to ensure unique names with NoteDb.
- groupConfig.setGroupUpdate(groupUpdate, this::getAccountNameEmail, this::getGroupName);
- commit(groupConfig);
+ GroupNameNotes groupNameNotes = null;
+ if (groupUpdate.getName().isPresent()) {
+ AccountGroup.NameKey oldName = originalGroup.getNameKey();
+ AccountGroup.NameKey newName = groupUpdate.getName().get();
+ groupNameNotes = GroupNameNotes.loadForRename(allUsersRepo, groupUuid, oldName, newName);
+ }
+
+ commit(allUsersRepo, groupConfig, groupNameNotes);
InternalGroup updatedGroup =
groupConfig
@@ -490,28 +519,41 @@
.setGroupId(updatedGroup.getId())
.setGroupName(updatedGroup.getNameKey())
.setModifiedMembers(modifiedMembers)
- .setModifiedSubgroups(modifiedSubgroups);
+ .setModifiedSubgroups(modifiedSubgroups)
+ .setRefState(updatedGroup.getRefState());
if (!Objects.equals(originalGroup.getNameKey(), updatedGroup.getNameKey())) {
resultBuilder.setPreviousGroupName(originalGroup.getNameKey());
}
return resultBuilder.build();
}
- private String getAccountNameEmail(Account.Id accountId) {
+ static String getAccountName(
+ AccountCache accountCache, String anonymousCowardName, Account.Id accountId) {
AccountState accountState = accountCache.getOrNull(accountId);
- String accountName =
- Optional.ofNullable(accountState)
- .map(AccountState::getAccount)
- .map(account -> account.getName(anonymousCowardName))
- .orElse(anonymousCowardName);
+ return Optional.ofNullable(accountState)
+ .map(AccountState::getAccount)
+ .map(account -> account.getName(anonymousCowardName))
+ .orElse(anonymousCowardName);
+ }
+
+ static String getAccountNameEmail(
+ AccountCache accountCache,
+ String anonymousCowardName,
+ Account.Id accountId,
+ String serverId) {
+ String accountName = getAccountName(accountCache, anonymousCowardName, accountId);
return formatNameEmail(accountName, getEmailForAuditLog(accountId, serverId));
}
- private static String getEmailForAuditLog(Account.Id accountId, String serverId) {
+ static String getEmailForAuditLog(Account.Id accountId, String serverId) {
return accountId.get() + "@" + serverId;
}
- private String getGroupName(AccountGroup.UUID groupUuid) {
+ private String getAccountNameEmail(Account.Id accountId) {
+ return getAccountNameEmail(accountCache, anonymousCowardName, accountId, serverId);
+ }
+
+ static String getGroupName(GroupCache groupCache, AccountGroup.UUID groupUuid) {
return groupCache
.get(groupUuid)
.map(InternalGroup::getName)
@@ -519,6 +561,10 @@
.orElse(groupUuid.get());
}
+ private String getGroupName(AccountGroup.UUID groupUuid) {
+ return getGroupName(groupCache, groupUuid);
+ }
+
private static String formatNameEmail(String name, String email) {
StringBuilder formattedResult = new StringBuilder();
PersonIdent.appendSanitized(formattedResult, name);
@@ -528,10 +574,25 @@
return formattedResult.toString();
}
- private void commit(GroupConfig groupConfig) throws IOException {
- try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(allUsersName)) {
+ private void commit(
+ Repository allUsersRepo, GroupConfig groupConfig, @Nullable GroupNameNotes groupNameNotes)
+ throws IOException {
+ BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
+ try (MetaDataUpdate metaDataUpdate =
+ metaDataUpdateFactory.create(allUsersName, allUsersRepo, batchRefUpdate)) {
groupConfig.commit(metaDataUpdate);
}
+ if (groupNameNotes != null) {
+ // MetaDataUpdates unfortunately can't be reused. -> Create a new one.
+ try (MetaDataUpdate metaDataUpdate =
+ metaDataUpdateFactory.create(allUsersName, allUsersRepo, batchRefUpdate)) {
+ groupNameNotes.commit(metaDataUpdate);
+ }
+ }
+
+ RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
+ gitRefUpdated.fire(
+ allUsersName, batchRefUpdate, currentUser != null ? currentUser.getAccount() : null);
}
private void updateCachesOnGroupCreation(InternalGroup createdGroup) throws IOException {
@@ -569,9 +630,17 @@
}
}
+ private void checkIfReviewDbUpdatesAreBlocked() throws OrmException {
+ if (reviewDbUpdatesAreBlocked) {
+ throw new OrmException("Updates to groups in ReviewDb are blocked");
+ }
+ }
+
@FunctionalInterface
private interface MetaDataUpdateFactory {
- MetaDataUpdate create(Project.NameKey projectName) throws IOException;
+ MetaDataUpdate create(
+ Project.NameKey projectName, Repository repository, BatchRefUpdate batchRefUpdate)
+ throws IOException;
}
@AutoValue
@@ -588,6 +657,9 @@
abstract ImmutableSet<AccountGroup.UUID> getModifiedSubgroups();
+ @Nullable
+ public abstract ObjectId getRefState();
+
static Builder builder() {
return new AutoValue_GroupsUpdate_UpdateResult.Builder();
}
@@ -606,6 +678,8 @@
abstract Builder setModifiedSubgroups(Set<AccountGroup.UUID> modifiedSubgroups);
+ public abstract Builder setRefState(ObjectId refState);
+
abstract UpdateResult build();
}
}
diff --git a/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java b/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
index f9fe4bb..13f84c1 100644
--- a/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
+++ b/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
@@ -65,8 +65,12 @@
public abstract Builder setMemberModification(MemberModification memberModification);
+ abstract MemberModification getMemberModification();
+
public abstract Builder setSubgroupModification(SubgroupModification subgroupModification);
+ abstract SubgroupModification getSubgroupModification();
+
public abstract InternalGroupUpdate build();
}
}
diff --git a/java/com/google/gerrit/server/group/db/testing/BUILD b/java/com/google/gerrit/server/group/db/testing/BUILD
new file mode 100644
index 0000000..1a5b598
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/testing/BUILD
@@ -0,0 +1,15 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+ name = "testing",
+ testonly = 1,
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
new file mode 100644
index 0000000..a71f417
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group.db.testing;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.group.db.GroupNameNotes.getGroupReference;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.CommitUtil;
+import java.io.IOException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Test utilities for low-level NoteDb groups. */
+public class GroupTestUtil {
+ public static ImmutableMap<String, String> readNameToUuidMap(Repository repo) throws Exception {
+ ImmutableMap.Builder<String, String> result = ImmutableMap.builder();
+ try (RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(RefNames.REFS_GROUPNAMES);
+ if (ref != null) {
+ NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(ref.getObjectId()));
+ for (Note note : noteMap) {
+ GroupReference gr = getGroupReference(rw.getObjectReader(), note.getData());
+ result.put(gr.getName(), gr.getUUID().get());
+ }
+ }
+ }
+ return result.build();
+ }
+
+ // TODO(dborowitz): Move somewhere even more common.
+ public static ImmutableList<CommitInfo> log(Repository repo, String refName) throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(refName);
+ if (ref != null) {
+ rw.sort(RevSort.REVERSE);
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ return Streams.stream(rw)
+ .map(
+ c -> {
+ try {
+ return CommitUtil.toCommitInfo(c);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "unexpected state when converting commit " + c.getName(), e);
+ }
+ })
+ .collect(toImmutableList());
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private GroupTestUtil() {}
+}
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index b37ed61..2abe876 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -30,12 +31,27 @@
import com.google.gerrit.server.query.change.SingleGroupUser;
import java.io.IOException;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.ConfigInvalidException;
public final class IndexUtils {
public static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
ImmutableMap.of("_", " ", ".", " ");
+ public static final Function<Exception, IOException> MAPPER =
+ new Function<Exception, IOException>() {
+ @Override
+ public IOException apply(Exception in) {
+ if (in instanceof IOException) {
+ return (IOException) in;
+ } else if (in instanceof ExecutionException && in.getCause() instanceof IOException) {
+ return (IOException) in.getCause();
+ } else {
+ return new IOException(in);
+ }
+ }
+ };
+
public static void setReady(SitePaths sitePaths, String name, int version, boolean ready)
throws IOException {
try {
@@ -57,10 +73,13 @@
}
public static Set<String> accountFields(QueryOptions opts) {
- Set<String> fs = opts.fields();
- return fs.contains(AccountField.ID.getName())
- ? fs
- : Sets.union(fs, ImmutableSet.of(AccountField.ID.getName()));
+ return accountFields(opts.fields());
+ }
+
+ public static Set<String> accountFields(Set<String> fields) {
+ return fields.contains(AccountField.ID.getName())
+ ? fields
+ : Sets.union(fields, ImmutableSet.of(AccountField.ID.getName()));
}
public static Set<String> changeFields(QueryOptions opts) {
diff --git a/java/com/google/gerrit/server/index/RefState.java b/java/com/google/gerrit/server/index/RefState.java
index b55afb6..6b893f0 100644
--- a/java/com/google/gerrit/server/index/RefState.java
+++ b/java/com/google/gerrit/server/index/RefState.java
@@ -19,9 +19,13 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Project;
import java.io.IOException;
+import java.util.List;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -29,6 +33,20 @@
@AutoValue
public abstract class RefState {
+ public static SetMultimap<Project.NameKey, RefState> parseStates(Iterable<byte[]> states) {
+ RefState.check(states != null, null);
+ SetMultimap<Project.NameKey, RefState> result =
+ MultimapBuilder.hashKeys().hashSetValues().build();
+ for (byte[] b : states) {
+ RefState.check(b != null, null);
+ String s = new String(b, UTF_8);
+ List<String> parts = Splitter.on(':').splitToList(s);
+ RefState.check(parts.size() == 3 && !parts.get(0).isEmpty() && !parts.get(1).isEmpty(), s);
+ result.put(new Project.NameKey(parts.get(0)), RefState.create(parts.get(1), parts.get(2)));
+ }
+ return result;
+ }
+
public static RefState create(String ref, String sha) {
return new AutoValue_RefState(ref, ObjectId.fromString(sha));
}
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index 6ec1260..b0527e1 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -14,7 +14,11 @@
package com.google.gerrit.server.index.account;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -22,11 +26,17 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexUtils;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.Config;
public class AccountIndexerImpl implements AccountIndexer {
public interface Factory {
@@ -37,6 +47,9 @@
private final AccountCache byIdCache;
private final DynamicSet<AccountIndexedListener> indexedListener;
+ private final StalenessChecker stalenessChecker;
+ private final ListeningExecutorService batchExecutor;
+ private final boolean autoReindexIfStale;
private final AccountIndexCollection indexes;
private final AccountIndex index;
@@ -44,9 +57,15 @@
AccountIndexerImpl(
AccountCache byIdCache,
DynamicSet<AccountIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @GerritServerConfig Config config,
@Assisted AccountIndexCollection indexes) {
this.byIdCache = byIdCache;
this.indexedListener = indexedListener;
+ this.stalenessChecker = stalenessChecker;
+ this.batchExecutor = batchExecutor;
+ this.autoReindexIfStale = autoReindexIfStale(config);
this.indexes = indexes;
this.index = null;
}
@@ -55,9 +74,15 @@
AccountIndexerImpl(
AccountCache byIdCache,
DynamicSet<AccountIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @GerritServerConfig Config config,
@Assisted AccountIndex index) {
this.byIdCache = byIdCache;
this.indexedListener = indexedListener;
+ this.stalenessChecker = stalenessChecker;
+ this.batchExecutor = batchExecutor;
+ this.autoReindexIfStale = autoReindexIfStale(config);
this.indexes = null;
this.index = index;
}
@@ -73,6 +98,44 @@
}
}
fireAccountIndexedEvent(id.get());
+ autoReindexIfStale(id);
+ }
+
+ private static boolean autoReindexIfStale(Config cfg) {
+ return cfg.getBoolean("index", null, "autoReindexIfStale", false);
+ }
+
+ private void autoReindexIfStale(Account.Id id) {
+ if (autoReindexIfStale) {
+ // Don't retry indefinitely; if this fails the account will be stale.
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError = reindexIfStale(id);
+ }
+ }
+
+ /**
+ * Asynchronously check if a account is stale, and reindex if it is.
+ *
+ * <p>Always run on the batch executor, even if this indexer instance is configured to use a
+ * different executor.
+ *
+ * @param id the ID of the account.
+ * @return future for reindexing the account; returns true if the account was stale.
+ */
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ Account.Id id) {
+ Callable<Boolean> task =
+ () -> {
+ if (stalenessChecker.isStale(id)) {
+ index(id);
+ return true;
+ }
+ return false;
+ };
+
+ return Futures.makeChecked(
+ Futures.nonCancellationPropagating(batchExecutor.submit(task)), IndexUtils.MAPPER);
}
private void fireAccountIndexedEvent(int id) {
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
new file mode 100644
index 0000000..6403d3d
--- /dev/null
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -0,0 +1,151 @@
+// 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.account;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.FieldBundle;
+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.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.index.RefState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Checks if documents in the account index are stale.
+ *
+ * <p>An index document is considered stale if the stored ref state differs from the SHA1 of the
+ * user branch or if the stored external ID states don't match with the external IDs of the account
+ * from the refs/meta/external-ids branch.
+ */
+@Singleton
+public class StalenessChecker {
+ public static final ImmutableSet<String> FIELDS =
+ ImmutableSet.of(
+ AccountField.ID.getName(),
+ AccountField.REF_STATE.getName(),
+ AccountField.EXTERNAL_ID_STATE.getName());
+
+ private final AccountIndexCollection indexes;
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final ExternalIds externalIds;
+ private final IndexConfig indexConfig;
+
+ @Inject
+ StalenessChecker(
+ AccountIndexCollection indexes,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ ExternalIds externalIds,
+ IndexConfig indexConfig) {
+ this.indexes = indexes;
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.externalIds = externalIds;
+ this.indexConfig = indexConfig;
+ }
+
+ public boolean isStale(Account.Id id) throws IOException {
+ AccountIndex i = indexes.getSearchIndex();
+ if (i == null) {
+ // No index; caller couldn't do anything if it is stale.
+ return false;
+ }
+ if (!i.getSchema().hasField(AccountField.REF_STATE)
+ || !i.getSchema().hasField(AccountField.EXTERNAL_ID_STATE)) {
+ // Index version not new enough for this check.
+ return false;
+ }
+
+ Optional<FieldBundle> result =
+ i.getRaw(id, QueryOptions.create(indexConfig, 0, 1, IndexUtils.accountFields(FIELDS)));
+ if (!result.isPresent()) {
+ // The document is missing in the index.
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ Ref ref = repo.exactRef(RefNames.refsUsers(id));
+
+ // Stale if the account actually exists.
+ return ref != null;
+ }
+ }
+
+ for (Map.Entry<Project.NameKey, RefState> e :
+ RefState.parseStates(result.get().getValue(AccountField.REF_STATE)).entries()) {
+ try (Repository repo = repoManager.openRepository(e.getKey())) {
+ if (!e.getValue().match(repo)) {
+ // Ref was modified since the account was indexed.
+ return true;
+ }
+ }
+ }
+
+ Set<ExternalId> extIds = externalIds.byAccount(id);
+ ListMultimap<ObjectId, ObjectId> extIdStates =
+ parseExternalIdStates(result.get().getValue(AccountField.EXTERNAL_ID_STATE));
+ if (extIdStates.size() != extIds.size()) {
+ // External IDs of the account were modified since the account was indexed.
+ return true;
+ }
+ for (ExternalId extId : extIds) {
+ if (!extIdStates.containsEntry(extId.key().sha1(), extId.blobId())) {
+ // External IDs of the account were modified since the account was indexed.
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static ListMultimap<ObjectId, ObjectId> parseExternalIdStates(
+ Iterable<byte[]> extIdStates) {
+ ListMultimap<ObjectId, ObjectId> result = MultimapBuilder.hashKeys().arrayListValues().build();
+
+ if (extIdStates == null) {
+ return result;
+ }
+
+ for (byte[] b : extIdStates) {
+ checkNotNull(b, "invalid external ID state");
+ String s = new String(b, UTF_8);
+ List<String> parts = Splitter.on(':').splitToList(s);
+ checkState(parts.size() == 2, "invalid external ID state: %s", s);
+ result.put(ObjectId.fromString(parts.get(0)), ObjectId.fromString(parts.get(1)));
+ }
+ return result;
+ }
+}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 37dd4a4..2444735 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -563,10 +563,10 @@
});
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
- SubmitRuleOptions.defaults().allowClosed(true).build();
+ SubmitRuleOptions.builder().allowClosed(true).build();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
- SubmitRuleOptions.defaults().build();
+ SubmitRuleOptions.builder().build();
/**
* JSON type for storing SubmitRecords.
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index f62b662..d205e91 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -17,7 +17,6 @@
import static com.google.gerrit.server.extensions.events.EventUtil.logEventListenerError;
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
-import com.google.common.base.Function;
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -31,6 +30,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.query.change.ChangeData;
@@ -50,7 +50,6 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jgit.lib.Config;
@@ -79,23 +78,9 @@
// ExecutionException, so we can reuse the same mapper as for a single
// future. Assume the actual contents of the exception are not useful to
// callers. All exceptions are already logged by IndexTask.
- return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+ return Futures.makeChecked(Futures.allAsList(futures), IndexUtils.MAPPER);
}
- private static final Function<Exception, IOException> MAPPER =
- new Function<Exception, IOException>() {
- @Override
- public IOException apply(Exception in) {
- if (in instanceof IOException) {
- return (IOException) in;
- } else if (in instanceof ExecutionException && in.getCause() instanceof IOException) {
- return (IOException) in.getCause();
- } else {
- return new IOException(in);
- }
- }
- };
-
private final ChangeIndexCollection indexes;
private final ChangeIndex index;
private final SchemaFactory<ReviewDb> schemaFactory;
@@ -335,7 +320,8 @@
@SuppressWarnings("deprecation")
private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
Callable<T> task, ListeningExecutorService executor) {
- return Futures.makeChecked(Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
+ return Futures.makeChecked(
+ Futures.nonCancellationPropagating(executor.submit(task)), IndexUtils.MAPPER);
}
private abstract class AbstractIndexTask<T> implements Callable<T> {
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index e804702..e7790df 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -158,21 +158,7 @@
}
private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) {
- return parseStates(cd.getRefStates());
- }
-
- public static SetMultimap<Project.NameKey, RefState> parseStates(Iterable<byte[]> states) {
- RefState.check(states != null, null);
- SetMultimap<Project.NameKey, RefState> result =
- MultimapBuilder.hashKeys().hashSetValues().build();
- for (byte[] b : states) {
- RefState.check(b != null, null);
- String s = new String(b, UTF_8);
- List<String> parts = Splitter.on(':').splitToList(s);
- RefState.check(parts.size() == 3 && !parts.get(0).isEmpty() && !parts.get(1).isEmpty(), s);
- result.put(new Project.NameKey(parts.get(0)), RefState.create(parts.get(1), parts.get(2)));
- }
- return result;
+ return RefState.parseStates(cd.getRefStates());
}
private ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(ChangeData cd) {
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 2ca0982..d10d70a 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -21,6 +21,7 @@
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.index.SiteIndexer;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,6 +33,7 @@
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -39,6 +41,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.slf4j.Logger;
@@ -73,7 +76,7 @@
List<AccountGroup.UUID> uuids;
try {
uuids = collectGroups(progress);
- } catch (OrmException e) {
+ } catch (OrmException | IOException | ConfigInvalidException e) {
log.error("Error collecting groups", e);
return new SiteIndexer.Result(sw, false, 0, 0);
}
@@ -128,10 +131,14 @@
return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get());
}
- private List<AccountGroup.UUID> collectGroups(ProgressMonitor progress) throws OrmException {
+ private List<AccountGroup.UUID> collectGroups(ProgressMonitor progress)
+ throws OrmException, IOException, ConfigInvalidException {
progress.beginTask("Collecting groups", ProgressMonitor.UNKNOWN);
try (ReviewDb db = schemaFactory.open()) {
- return groups.getAllUuids(db).collect(toImmutableList());
+ return groups
+ .getAllGroupReferences(db)
+ .map(GroupReference::getUUID)
+ .collect(toImmutableList());
} finally {
progress.endTask();
}
diff --git a/java/com/google/gerrit/server/index/group/GroupField.java b/java/com/google/gerrit/server/index/group/GroupField.java
index 078433a..29e3867 100644
--- a/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/java/com/google/gerrit/server/index/group/GroupField.java
@@ -19,14 +19,18 @@
import static com.google.gerrit.index.FieldDef.fullText;
import static com.google.gerrit.index.FieldDef.integer;
import static com.google.gerrit.index.FieldDef.prefix;
+import static com.google.gerrit.index.FieldDef.storedOnly;
import static com.google.gerrit.index.FieldDef.timestamp;
+import com.google.common.base.MoreObjects;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.InternalGroup;
import java.sql.Timestamp;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
/** Secondary index schemas for groups. */
public class GroupField {
@@ -72,4 +76,14 @@
.buildRepeatable(
g ->
g.getSubgroups().stream().map(AccountGroup.UUID::get).collect(toImmutableList()));
+
+ /** ObjectId of HEAD:refs/groups/<UUID>. */
+ public static final FieldDef<InternalGroup, byte[]> REF_STATE =
+ storedOnly("ref_state")
+ .build(
+ g -> {
+ byte[] a = new byte[Constants.OBJECT_ID_STRING_LENGTH];
+ MoreObjects.firstNonNull(g.getRefState(), ObjectId.zeroId()).copyTo(a, 0);
+ return a;
+ });
}
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index 69b29bc..58caeb2 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -14,20 +14,30 @@
package com.google.gerrit.server.index.group;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.events.GroupIndexedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.index.Index;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexUtils;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.Config;
public class GroupIndexerImpl implements GroupIndexer {
public interface Factory {
@@ -39,16 +49,25 @@
private final GroupCache groupCache;
private final DynamicSet<GroupIndexedListener> indexedListener;
private final GroupIndexCollection indexes;
+ private final StalenessChecker stalenessChecker;
+ private final boolean autoReindexIfStale;
private final GroupIndex index;
+ private final ListeningExecutorService batchExecutor;
@AssistedInject
GroupIndexerImpl(
GroupCache groupCache,
DynamicSet<GroupIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @GerritServerConfig Config config,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
@Assisted GroupIndexCollection indexes) {
this.groupCache = groupCache;
this.indexedListener = indexedListener;
this.indexes = indexes;
+ this.stalenessChecker = stalenessChecker;
+ this.autoReindexIfStale = autoReindexIfStale(config);
+ this.batchExecutor = batchExecutor;
this.index = null;
}
@@ -56,10 +75,16 @@
GroupIndexerImpl(
GroupCache groupCache,
DynamicSet<GroupIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @GerritServerConfig Config config,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
@Assisted GroupIndex index) {
this.groupCache = groupCache;
this.indexedListener = indexedListener;
this.indexes = null;
+ this.stalenessChecker = stalenessChecker;
+ this.autoReindexIfStale = autoReindexIfStale(config);
+ this.batchExecutor = batchExecutor;
this.index = index;
}
@@ -74,6 +99,44 @@
}
}
fireGroupIndexedEvent(uuid.get());
+ autoReindexIfStale(uuid);
+ }
+
+ private static boolean autoReindexIfStale(Config cfg) {
+ return cfg.getBoolean("index", null, "autoReindexIfStale", true);
+ }
+
+ private void autoReindexIfStale(AccountGroup.UUID uuid) {
+ if (autoReindexIfStale) {
+ // Don't retry indefinitely; if this fails the group will be stale.
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError = reindexIfStale(uuid);
+ }
+ }
+
+ /**
+ * Asynchronously check if a group is stale, and reindex if it is.
+ *
+ * <p>Always run on the batch executor, even if this indexer instance is configured to use a
+ * different executor.
+ *
+ * @param uuid the unique identifier of the group.
+ * @return future for reindexing the group; returns true if the group was stale.
+ */
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ AccountGroup.UUID uuid) {
+ Callable<Boolean> task =
+ () -> {
+ if (stalenessChecker.isStale(uuid)) {
+ index(uuid);
+ return true;
+ }
+ return false;
+ };
+
+ return Futures.makeChecked(
+ Futures.nonCancellationPropagating(batchExecutor.submit(task)), IndexUtils.MAPPER);
}
private void fireGroupIndexedEvent(String uuid) {
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index b280b25..912524f 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -34,8 +34,11 @@
@Deprecated static final Schema<InternalGroup> V3 = schema(V2, GroupField.CREATED_ON);
+ @Deprecated
static final Schema<InternalGroup> V4 = schema(V3, GroupField.MEMBER, GroupField.SUBGROUP);
+ static final Schema<InternalGroup> V5 = schema(V4, GroupField.REF_STATE);
+
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
private GroupSchemaDefinitions() {
diff --git a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
index 255df32..79f25c0 100644
--- a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
+++ b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.index.group;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.IndexedQuery;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.query.DataSource;
@@ -22,10 +23,22 @@
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.InternalGroup;
+import java.util.HashSet;
+import java.util.Set;
public class IndexedGroupQuery extends IndexedQuery<AccountGroup.UUID, InternalGroup>
implements DataSource<InternalGroup> {
+ public static QueryOptions createOptions(
+ IndexConfig config, int start, int limit, Set<String> fields) {
+ // Always include GroupField.UUID since it is needed to load the group from NoteDb.
+ if (!fields.contains(GroupField.UUID.getName())) {
+ fields = new HashSet<>(fields);
+ fields.add(GroupField.UUID.getName());
+ }
+ return QueryOptions.create(config, start, limit, fields);
+ }
+
public IndexedGroupQuery(
Index<AccountGroup.UUID, InternalGroup> index,
Predicate<InternalGroup> pred,
diff --git a/java/com/google/gerrit/server/index/group/StalenessChecker.java b/java/com/google/gerrit/server/index/group/StalenessChecker.java
new file mode 100644
index 0000000..418bb35
--- /dev/null
+++ b/java/com/google/gerrit/server/index/group/StalenessChecker.java
@@ -0,0 +1,97 @@
+// 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.group;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.GroupsMigration;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Checks if documents in the group index are stale.
+ *
+ * <p>An index document is considered stale if the stored SHA1 differs from the HEAD SHA1 of the
+ * groups branch.
+ *
+ * <p>Note: This only applies to NoteDb.
+ */
+@Singleton
+public class StalenessChecker {
+ public static final ImmutableSet<String> FIELDS =
+ ImmutableSet.of(GroupField.UUID.getName(), GroupField.REF_STATE.getName());
+
+ private final GroupIndexCollection indexes;
+ private final GitRepositoryManager repoManager;
+ private final IndexConfig indexConfig;
+ private final AllUsersName allUsers;
+ private final GroupsMigration groupsMigration;
+
+ @Inject
+ StalenessChecker(
+ GroupIndexCollection indexes,
+ GitRepositoryManager repoManager,
+ IndexConfig indexConfig,
+ AllUsersName allUsers,
+ GroupsMigration groupsMigration) {
+ this.indexes = indexes;
+ this.repoManager = repoManager;
+ this.indexConfig = indexConfig;
+ this.allUsers = allUsers;
+ this.groupsMigration = groupsMigration;
+ }
+
+ public boolean isStale(AccountGroup.UUID uuid) throws IOException {
+ if (!groupsMigration.readFromNoteDb()) {
+ return false; // This class only treats staleness for groups in NoteDb.
+ }
+
+ GroupIndex i = indexes.getSearchIndex();
+ if (i == null) {
+ return false; // No index; caller couldn't do anything if it is stale.
+ }
+ if (!i.getSchema().hasField(GroupField.REF_STATE)) {
+ return false; // Index version not new enough for this check.
+ }
+
+ Optional<FieldBundle> result =
+ i.getRaw(uuid, IndexedGroupQuery.createOptions(indexConfig, 0, 1, FIELDS));
+ if (!result.isPresent()) {
+ // The document is missing in the index.
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
+
+ // Stale if the group actually exists.
+ return ref != null;
+ }
+ }
+
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
+ ObjectId head = ref == null ? ObjectId.zeroId() : ref.getObjectId();
+ return !head.equals(ObjectId.fromString(result.get().getValue(GroupField.REF_STATE), 0));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeBundle.java b/java/com/google/gerrit/server/notedb/ChangeBundle.java
index a9663c7..7714c6e 100644
--- a/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -17,8 +17,8 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.common.TimeUtil.roundToSecond;
+import static com.google.gerrit.common.TimeUtil.truncateToSecond;
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
@@ -71,7 +71,6 @@
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
-import java.util.TreeSet;
/**
* A bundle of all entities rooted at a single {@link Change} entity.
@@ -212,23 +211,6 @@
.compare(a.get(), b.get());
}
- private static void checkColumns(Class<?> clazz, Integer... expected) {
- Set<Integer> ids = new TreeSet<>();
- for (Field f : clazz.getDeclaredFields()) {
- Column col = f.getAnnotation(Column.class);
- if (col != null) {
- ids.add(col.id());
- }
- }
- Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
- checkState(
- ids.equals(expectedIds),
- "Unexpected column set for %s: %s != %s",
- clazz.getSimpleName(),
- ids,
- expectedIds);
- }
-
static {
// Initialization-time checks that the column set hasn't changed since the
// last time this file was updated.
@@ -958,7 +940,7 @@
// seconds apart, the timestamp in NoteDb may actually be several seconds
// *earlier* than the timestamp in ReviewDb that it was converted from.
checkArgument(
- tsFromNoteDb.equals(roundToSecond(tsFromNoteDb)),
+ tsFromNoteDb.equals(truncateToSecond(tsFromNoteDb)),
"%s from NoteDb has non-rounded %s timestamp: %s",
desc,
field,
diff --git a/java/com/google/gerrit/server/notedb/GroupsMigration.java b/java/com/google/gerrit/server/notedb/GroupsMigration.java
new file mode 100644
index 0000000..293f3c6
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/GroupsMigration.java
@@ -0,0 +1,82 @@
+// 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;
+
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class GroupsMigration {
+ public static class Module extends AbstractModule {
+ @Override
+ public void configure() {
+ bind(GroupsMigration.class);
+ }
+ }
+
+ private final boolean writeToNoteDb;
+ private final boolean readFromNoteDb;
+ private final boolean disableGroupReviewDb;
+
+ @Inject
+ public GroupsMigration(@GerritServerConfig Config cfg) {
+ // TODO(aliceks): Remove these flags when all other necessary TODOs for writing groups to
+ // NoteDb have been addressed.
+ // Don't flip these flags in a production setting! We only added them to spread the
+ // implementation of groups in NoteDb among several changes which are gradually merged.
+ this(
+ cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false),
+ cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false),
+ cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false));
+ }
+
+ public GroupsMigration(
+ boolean writeToNoteDb, boolean readFromNoteDb, boolean disableGroupReviewDb) {
+ this.writeToNoteDb = writeToNoteDb;
+ this.readFromNoteDb = readFromNoteDb;
+ this.disableGroupReviewDb = disableGroupReviewDb;
+ }
+
+ public boolean writeToNoteDb() {
+ return writeToNoteDb;
+ }
+
+ public boolean readFromNoteDb() {
+ return readFromNoteDb;
+ }
+
+ public boolean disableGroupReviewDb() {
+ return disableGroupReviewDb;
+ }
+
+ public void setConfigValuesIfNotSetYet(Config cfg) {
+ Set<String> subsections = cfg.getSubsections(SECTION_NOTE_DB);
+ if (!subsections.contains(GROUPS.key())) {
+ cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, writeToNoteDb());
+ cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, readFromNoteDb());
+ cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, disableGroupReviewDb());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbTable.java b/java/com/google/gerrit/server/notedb/NoteDbTable.java
index be24e28..e299fdf 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbTable.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbTable.java
@@ -16,6 +16,7 @@
public enum NoteDbTable {
ACCOUNTS,
+ GROUPS,
CHANGES;
public String key() {
diff --git a/java/com/google/gerrit/server/notedb/NotesMigration.java b/java/com/google/gerrit/server/notedb/NotesMigration.java
index e560ec8..9cee2cd 100644
--- a/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -54,12 +54,12 @@
*/
public abstract class NotesMigration {
public static final String SECTION_NOTE_DB = "noteDb";
+ public static final String READ = "read";
+ public static final String WRITE = "write";
+ public static final String DISABLE_REVIEW_DB = "disableReviewDb";
- private static final String DISABLE_REVIEW_DB = "disableReviewDb";
private static final String PRIMARY_STORAGE = "primaryStorage";
- private static final String READ = "read";
private static final String SEQUENCE = "sequence";
- private static final String WRITE = "write";
public static class Module extends AbstractModule {
@Override
diff --git a/java/com/google/gerrit/server/patch/PatchListLoader.java b/java/com/google/gerrit/server/patch/PatchListLoader.java
index 8fce6d3..bf6e345 100644
--- a/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -510,7 +510,7 @@
byte[] aContent = aText.getContent();
byte[] bContent = bText.getContent();
long size = bContent.length;
- long sizeDelta = bContent.length - aContent.length;
+ long sizeDelta = size - aContent.length;
RawText aRawText = new RawText(aContent);
RawText bRawText = new RawText(bContent);
EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
diff --git a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
index 5f97134..8e162ba 100644
--- a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
+++ b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
@@ -16,11 +16,8 @@
import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
-import com.google.gerrit.extensions.api.plugins.Plugins;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.api.plugins.PluginApiImpl;
-import com.google.gerrit.server.api.plugins.PluginsImpl;
public class PluginRestApiModule extends RestApiModule {
@Override
@@ -33,7 +30,5 @@
post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
- bind(Plugins.class).to(PluginsImpl.class);
- factory(PluginApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/project/GetCommit.java b/java/com/google/gerrit/server/project/GetCommit.java
index 2afeb07..d8fc5b6 100644
--- a/java/com/google/gerrit/server/project/GetCommit.java
+++ b/java/com/google/gerrit/server/project/GetCommit.java
@@ -18,12 +18,13 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.git.CommitUtil;
import com.google.inject.Singleton;
+import java.io.IOException;
@Singleton
public class GetCommit implements RestReadView<CommitResource> {
@Override
- public CommitInfo apply(CommitResource rsrc) {
+ public CommitInfo apply(CommitResource rsrc) throws IOException {
return CommitUtil.toCommitInfo(rsrc.getCommit());
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 3fe838d..4b2161b 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -134,6 +134,7 @@
try {
return checkedGet(projectName);
} catch (IOException e) {
+ log.warn("Cannot read project " + projectName, e);
return null;
}
}
@@ -232,6 +233,7 @@
try {
src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
} catch (ExecutionException e) {
+ log.warn("Cannot look up projects for prefix " + pfx, e);
return Collections.emptyList();
}
return new Iterable<Project.NameKey>() {
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 398abd5..8ca3932 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -97,7 +97,7 @@
private final ProjectCache projectCache;
private final ChangeData cd;
- private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.defaults();
+ private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.builder();
private SubmitRuleOptions opts;
private Change change;
private CurrentUser user;
@@ -142,7 +142,7 @@
if (opts != null) {
optsBuilder = opts.toBuilder();
} else {
- optsBuilder = SubmitRuleOptions.defaults();
+ optsBuilder = SubmitRuleOptions.builder();
}
return this;
}
diff --git a/java/com/google/gerrit/server/project/SubmitRuleOptions.java b/java/com/google/gerrit/server/project/SubmitRuleOptions.java
index cb44b28..332aa75 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleOptions.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleOptions.java
@@ -26,11 +26,10 @@
@AutoValue
public abstract class SubmitRuleOptions {
public static Builder builder() {
- return new AutoValue_SubmitRuleOptions.Builder();
- }
-
- public static Builder defaults() {
- return builder().allowClosed(false).skipFilters(false).rule(null);
+ return new AutoValue_SubmitRuleOptions.Builder()
+ .allowClosed(false)
+ .skipFilters(false)
+ .rule(null);
}
public abstract boolean allowClosed();
@@ -40,6 +39,8 @@
@Nullable
public abstract String rule();
+ public abstract Builder toBuilder();
+
@AutoValue.Builder
public abstract static class Builder {
public abstract SubmitRuleOptions.Builder allowClosed(boolean allowClosed);
@@ -50,8 +51,4 @@
public abstract SubmitRuleOptions build();
}
-
- public Builder toBuilder() {
- return builder().allowClosed(allowClosed()).skipFilters(skipFilters()).rule(rule());
- }
}
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index a703852..b3e1c27 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.OrPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.reviewdb.client.Change;
@@ -54,6 +55,11 @@
}
@Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
public boolean hasChange() {
for (Predicate<ChangeData> p : getChildren()) {
if (!(p instanceof ChangeDataSource) || !((ChangeDataSource) p).hasChange()) {
diff --git a/java/com/google/gerrit/server/schema/AbstractDisabledAccess.java b/java/com/google/gerrit/server/schema/AbstractDisabledAccess.java
new file mode 100644
index 0000000..17eb56e
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/AbstractDisabledAccess.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gwtorm.client.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import java.util.Map;
+import java.util.function.Function;
+
+abstract class AbstractDisabledAccess<T, K extends Key<?>> implements Access<T, K> {
+ private static <T> ResultSet<T> empty() {
+ return new ListResultSet<>(ImmutableList.of());
+ }
+
+ @SuppressWarnings("deprecation")
+ private static <T>
+ com.google.common.util.concurrent.CheckedFuture<T, OrmException> emptyFuture() {
+ return Futures.immediateCheckedFuture(null);
+ }
+
+ // Don't even hold a reference to delegate, so it's not possible to use it
+ // accidentally.
+ private final ReviewDbWrapper wrapper;
+ private final String relationName;
+ private final int relationId;
+ private final Function<T, K> primaryKey;
+ private final Function<Iterable<T>, Map<K, T>> toMap;
+
+ AbstractDisabledAccess(ReviewDbWrapper wrapper, Access<T, K> delegate) {
+ this.wrapper = wrapper;
+ this.relationName = delegate.getRelationName();
+ this.relationId = delegate.getRelationID();
+ this.primaryKey = delegate::primaryKey;
+ this.toMap = delegate::toMap;
+ }
+
+ @Override
+ public final int getRelationID() {
+ return relationId;
+ }
+
+ @Override
+ public final String getRelationName() {
+ return relationName;
+ }
+
+ @Override
+ public final K primaryKey(T entity) {
+ return primaryKey.apply(entity);
+ }
+
+ @Override
+ public final Map<K, T> toMap(Iterable<T> iterable) {
+ return toMap.apply(iterable);
+ }
+
+ @Override
+ public final ResultSet<T> iterateAllEntities() {
+ return empty();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public final com.google.common.util.concurrent.CheckedFuture<T, OrmException> getAsync(K key) {
+ return emptyFuture();
+ }
+
+ @Override
+ public final ResultSet<T> get(Iterable<K> keys) {
+ return empty();
+ }
+
+ @Override
+ public final void insert(Iterable<T> instances) {
+ // Do nothing.
+ }
+
+ @Override
+ public final void update(Iterable<T> instances) {
+ // Do nothing.
+ }
+
+ @Override
+ public final void upsert(Iterable<T> instances) {
+ // Do nothing.
+ }
+
+ @Override
+ public final void deleteKeys(Iterable<K> keys) {
+ // Do nothing.
+ }
+
+ @Override
+ public final void delete(Iterable<T> instances) {
+ // Do nothing.
+ }
+
+ @Override
+ public final void beginTransaction(K key) {
+ // Keep track of when we've started a transaction so that we can avoid calling commit/rollback
+ // on the underlying ReviewDb. This is just a simple arm's-length approach, and may produce
+ // slightly different results from a native ReviewDb in corner cases like:
+ // * beginning transactions on different tables simultaneously
+ // * doing work between commit and rollback
+ // These kinds of things are already misuses of ReviewDb, and shouldn't be happening in current
+ // code anyway.
+ checkState(!wrapper.inTransaction(), "already in transaction");
+ wrapper.beginTransaction();
+ }
+
+ @Override
+ public final T atomicUpdate(K key, AtomicUpdate<T> update) {
+ return null;
+ }
+
+ @Override
+ public final T get(K id) {
+ return null;
+ }
+}
diff --git a/java/com/google/gerrit/server/schema/AllUsersCreator.java b/java/com/google/gerrit/server/schema/AllUsersCreator.java
index 3049e86..42c86c1 100644
--- a/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -112,6 +112,12 @@
grant(config, defaults, Permission.CREATE, admin);
}
+ // Grant read permissions on the group branches to all users.
+ // This allows group owners to see the group refs. VisibleRefFilter ensures that read
+ // permissions for non-group-owners are ignored.
+ AccessSection groups = config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
+ grant(config, groups, Permission.READ, false, true, registered);
+
config.commit(md);
}
}
diff --git a/java/com/google/gerrit/server/schema/DataSourceProvider.java b/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 67f6894..98acc64 100644
--- a/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -138,6 +138,9 @@
MILLISECONDS.convert(30, SECONDS),
MILLISECONDS));
ds.setInitialSize(ds.getMinIdle());
+ long evictIdleTimeMs = 1000L * 60;
+ ds.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
+ ds.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
ds.setValidationQuery(dst.getValidationQuery());
ds.setValidationQueryTimeout(5);
exportPoolMetrics(ds);
diff --git a/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java b/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
index fd0c7fc..7247490 100644
--- a/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
+++ b/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
@@ -14,10 +14,7 @@
package com.google.gerrit.server.schema;
-import static com.google.common.base.Preconditions.checkState;
-
import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -31,14 +28,9 @@
import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gwtorm.client.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
-import java.util.Map;
-import java.util.function.Function;
/**
* Wrapper for ReviewDb that never calls the underlying change tables.
@@ -50,20 +42,12 @@
return new ListResultSet<>(ImmutableList.of());
}
- @SuppressWarnings("deprecation")
- private static <T, K extends Key<?>>
- com.google.common.util.concurrent.CheckedFuture<T, OrmException> emptyFuture() {
- return Futures.immediateCheckedFuture(null);
- }
-
private final ChangeAccess changes;
private final PatchSetApprovalAccess patchSetApprovals;
private final ChangeMessageAccess changeMessages;
private final PatchSetAccess patchSets;
private final PatchLineCommentAccess patchComments;
- private boolean inTransaction;
-
NoChangesReviewDbWrapper(ReviewDb db) {
super(db);
changes = new Changes(this, delegate);
@@ -74,11 +58,6 @@
}
@Override
- public boolean changesTablesEnabled() {
- return false;
- }
-
- @Override
public ChangeAccess changes() {
return changes;
}
@@ -103,128 +82,6 @@
return patchComments;
}
- @Override
- public void commit() throws OrmException {
- if (!inTransaction) {
- // This reads a little weird, we're not in a transaction, so why are we calling commit?
- // Because we want to let the underlying ReviewDb do its normal thing in this case (which may
- // be throwing an exception, or not, depending on implementation).
- delegate.commit();
- }
- }
-
- @Override
- public void rollback() throws OrmException {
- if (inTransaction) {
- inTransaction = false;
- } else {
- // See comment in commit(): we want to let the underlying ReviewDb do its thing.
- delegate.rollback();
- }
- }
-
- private abstract static class AbstractDisabledAccess<T, K extends Key<?>>
- implements Access<T, K> {
- // Don't even hold a reference to delegate, so it's not possible to use it accidentally.
- private final NoChangesReviewDbWrapper wrapper;
- private final String relationName;
- private final int relationId;
- private final Function<T, K> primaryKey;
- private final Function<Iterable<T>, Map<K, T>> toMap;
-
- private AbstractDisabledAccess(NoChangesReviewDbWrapper wrapper, Access<T, K> delegate) {
- this.wrapper = wrapper;
- this.relationName = delegate.getRelationName();
- this.relationId = delegate.getRelationID();
- this.primaryKey = delegate::primaryKey;
- this.toMap = delegate::toMap;
- }
-
- @Override
- public final int getRelationID() {
- return relationId;
- }
-
- @Override
- public final String getRelationName() {
- return relationName;
- }
-
- @Override
- public final K primaryKey(T entity) {
- return primaryKey.apply(entity);
- }
-
- @Override
- public final Map<K, T> toMap(Iterable<T> iterable) {
- return toMap.apply(iterable);
- }
-
- @Override
- public final ResultSet<T> iterateAllEntities() {
- return empty();
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public final com.google.common.util.concurrent.CheckedFuture<T, OrmException> getAsync(K key) {
- return emptyFuture();
- }
-
- @Override
- public final ResultSet<T> get(Iterable<K> keys) {
- return empty();
- }
-
- @Override
- public final void insert(Iterable<T> instances) {
- // Do nothing.
- }
-
- @Override
- public final void update(Iterable<T> instances) {
- // Do nothing.
- }
-
- @Override
- public final void upsert(Iterable<T> instances) {
- // Do nothing.
- }
-
- @Override
- public final void deleteKeys(Iterable<K> keys) {
- // Do nothing.
- }
-
- @Override
- public final void delete(Iterable<T> instances) {
- // Do nothing.
- }
-
- @Override
- public final void beginTransaction(K key) {
- // Keep track of when we've started a transaction so that we can avoid calling commit/rollback
- // on the underlying ReviewDb. This is just a simple arm's-length approach, and may produce
- // slightly different results from a native ReviewDb in corner cases like:
- // * beginning transactions on different tables simultaneously
- // * doing work between commit and rollback
- // These kinds of things are already misuses of ReviewDb, and shouldn't be happening in
- // current code anyway.
- checkState(!wrapper.inTransaction, "already in transaction");
- wrapper.inTransaction = true;
- }
-
- @Override
- public final T atomicUpdate(K key, AtomicUpdate<T> update) {
- return null;
- }
-
- @Override
- public final T get(K id) {
- return null;
- }
- }
-
private static class Changes extends AbstractDisabledAccess<Change, Change.Id>
implements ChangeAccess {
private Changes(NoChangesReviewDbWrapper wrapper, ReviewDb db) {
diff --git a/java/com/google/gerrit/server/schema/NoGroupsReviewDbWrapper.java b/java/com/google/gerrit/server/schema/NoGroupsReviewDbWrapper.java
new file mode 100644
index 0000000..33c4d77
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/NoGroupsReviewDbWrapper.java
@@ -0,0 +1,202 @@
+// 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.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.AccountGroupAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+/**
+ * Wrapper for ReviewDb that never calls the underlying groups tables.
+ *
+ * <p>See {@link NotesMigrationSchemaFactory} for discussion.
+ */
+public class NoGroupsReviewDbWrapper extends ReviewDbWrapper {
+ private static <T> ResultSet<T> empty() {
+ return new ListResultSet<>(ImmutableList.of());
+ }
+
+ private final AccountGroupAccess groups;
+ private final AccountGroupNameAccess groupNames;
+ private final AccountGroupMemberAccess members;
+ private final AccountGroupMemberAuditAccess memberAudits;
+ private final AccountGroupByIdAccess byIds;
+ private final AccountGroupByIdAudAccess byIdAudits;
+
+ protected NoGroupsReviewDbWrapper(ReviewDb db) {
+ super(db);
+ this.groups = new Groups(this, delegate);
+ this.groupNames = new GroupNames(this, delegate);
+ this.members = new Members(this, delegate);
+ this.memberAudits = new MemberAudits(this, delegate);
+ this.byIds = new ByIds(this, delegate);
+ this.byIdAudits = new ByIdAudits(this, delegate);
+ }
+
+ @Override
+ public AccountGroupAccess accountGroups() {
+ return groups;
+ }
+
+ @Override
+ public AccountGroupNameAccess accountGroupNames() {
+ return groupNames;
+ }
+
+ @Override
+ public AccountGroupMemberAccess accountGroupMembers() {
+ return members;
+ }
+
+ @Override
+ public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
+ return memberAudits;
+ }
+
+ @Override
+ public AccountGroupByIdAudAccess accountGroupByIdAud() {
+ return byIdAudits;
+ }
+
+ @Override
+ public AccountGroupByIdAccess accountGroupById() {
+ return byIds;
+ }
+
+ private static class Groups extends AbstractDisabledAccess<AccountGroup, AccountGroup.Id>
+ implements AccountGroupAccess {
+ private Groups(ReviewDbWrapper wrapper, ReviewDb db) {
+ super(wrapper, db.accountGroups());
+ }
+
+ @Override
+ public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException {
+ return empty();
+ }
+
+ @Override
+ public ResultSet<AccountGroup> all() throws OrmException {
+ return empty();
+ }
+ }
+
+ private static class GroupNames
+ extends AbstractDisabledAccess<AccountGroupName, AccountGroup.NameKey>
+ implements AccountGroupNameAccess {
+ private GroupNames(ReviewDbWrapper wrapper, ReviewDb db) {
+ super(wrapper, db.accountGroupNames());
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> all() throws OrmException {
+ return empty();
+ }
+ }
+
+ private static class Members
+ extends AbstractDisabledAccess<AccountGroupMember, AccountGroupMember.Key>
+ implements AccountGroupMemberAccess {
+ private Members(ReviewDbWrapper wrapper, ReviewDb db) {
+ super(wrapper, db.accountGroupMembers());
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException {
+ return empty();
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException {
+ return empty();
+ }
+ }
+
+ private static class MemberAudits
+ extends AbstractDisabledAccess<AccountGroupMemberAudit, AccountGroupMemberAudit.Key>
+ implements AccountGroupMemberAuditAccess {
+ private MemberAudits(ReviewDbWrapper wrapper, ReviewDb db) {
+ super(wrapper, db.accountGroupMembersAudit());
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroupAccount(
+ AccountGroup.Id groupId, com.google.gerrit.reviewdb.client.Account.Id accountId)
+ throws OrmException {
+ return empty();
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) throws OrmException {
+ return empty();
+ }
+ }
+
+ private static class ByIds extends AbstractDisabledAccess<AccountGroupById, AccountGroupById.Key>
+ implements AccountGroupByIdAccess {
+ private ByIds(ReviewDbWrapper wrapper, ReviewDb db) {
+ super(wrapper, db.accountGroupById());
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException {
+ return empty();
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException {
+ return empty();
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> all() throws OrmException {
+ return empty();
+ }
+ }
+
+ private static class ByIdAudits
+ extends AbstractDisabledAccess<AccountGroupByIdAud, AccountGroupByIdAud.Key>
+ implements AccountGroupByIdAudAccess {
+ private ByIdAudits(ReviewDbWrapper wrapper, ReviewDb db) {
+ super(wrapper, db.accountGroupByIdAud());
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroupInclude(
+ AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException {
+ return empty();
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) throws OrmException {
+ return empty();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
index d73a5f4..9bc8b61 100644
--- a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
+++ b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
@@ -15,7 +15,9 @@
package com.google.gerrit.server.schema;
import com.google.gerrit.reviewdb.server.DisallowReadFromChangesReviewDbWrapper;
+import com.google.gerrit.reviewdb.server.DisallowReadFromGroupsReviewDbWrapper;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -26,21 +28,20 @@
public class NotesMigrationSchemaFactory implements SchemaFactory<ReviewDb> {
private final SchemaFactory<ReviewDb> delegate;
private final NotesMigration migration;
+ private final GroupsMigration groupsMigration;
@Inject
NotesMigrationSchemaFactory(
- @ReviewDbFactory SchemaFactory<ReviewDb> delegate, NotesMigration migration) {
+ @ReviewDbFactory SchemaFactory<ReviewDb> delegate,
+ NotesMigration migration,
+ GroupsMigration groupsMigration) {
this.delegate = delegate;
this.migration = migration;
+ this.groupsMigration = groupsMigration;
}
@Override
public ReviewDb open() throws OrmException {
- ReviewDb db = delegate.open();
- if (!migration.readChanges()) {
- return db;
- }
-
// There are two levels at which this class disables access to Changes and related tables,
// corresponding to two phases of the NoteDb migration:
//
@@ -50,24 +51,45 @@
// support writing to ReviewDb. This behavior is accomplished by wrapping in a
// DisallowReadFromChangesReviewDbWrapper.
//
- // Some codepaths might need to be able to read from ReviewDb if they really need to, because
- // they need to operate on the underlying source of truth, for example when reading a change
- // to determine its primary storage. To support this, ReviewDbUtil#unwrapDb can detect and
- // unwrap databases of this type.
+ // Some codepaths might need to be able to read from ReviewDb if they really need to,
+ // because they need to operate on the underlying source of truth, for example when reading
+ // a change to determine its primary storage. To support this, ReviewDbUtil#unwrapDb can
+ // detect and unwrap databases of this type.
//
// 2. After all changes have their primary storage in NoteDb, we can completely shut off access
// to the change tables. At this point in the migration, we are by definition not using the
// ReviewDb tables at all; we could even delete the tables at this point, and Gerrit would
// continue to function.
//
- // This is accomplished by setting the delegate ReviewDb *underneath* DisallowReadFromChanges
- // to be a complete no-op, with NoChangesReviewDbWrapper. With this wrapper, all read
- // operations return no results, and write operations silently do nothing. This wrapper is
- // not a public class and nobody should ever attempt to unwrap it.
+ // This is accomplished by setting the delegate ReviewDb *underneath*
+ // DisallowReadFromChanges to be a complete no-op, with NoChangesReviewDbWrapper. With this
+ // wrapper, all read operations return no results, and write operations silently do nothing.
+ // This wrapper is not a public class and nobody should ever attempt to unwrap it.
- if (migration.disableChangeReviewDb()) {
+ // First create the wrappers which can not be removed by ReviewDbUtil#unwrapDb(ReviewDb).
+ ReviewDb db = delegate.open();
+ if (migration.readChanges() && migration.disableChangeReviewDb()) {
+ // Disable writes to change tables in ReviewDb (ReviewDb access for changes are No-Ops).
db = new NoChangesReviewDbWrapper(db);
}
- return new DisallowReadFromChangesReviewDbWrapper(db);
+
+ if (groupsMigration.readFromNoteDb() && groupsMigration.disableGroupReviewDb()) {
+ // Disable writes to group tables in ReviewDb (ReviewDb access for groups are No-Ops).
+ db = new NoGroupsReviewDbWrapper(db);
+ }
+
+ // Second create the wrappers which can be removed by ReviewDbUtil#unwrapDb(ReviewDb).
+ if (migration.readChanges()) {
+ // If reading changes from NoteDb is configured, changes should not be read from ReviewDb.
+ // Make sure that any attempt to read a change from ReviewDb anyway fails with an exception.
+ db = new DisallowReadFromChangesReviewDbWrapper(db);
+ }
+
+ if (groupsMigration.readFromNoteDb()) {
+ // If reading groups from NoteDb is configured, groups should not be read from ReviewDb.
+ // Make sure that any attempt to read a group from ReviewDb anyway fails with an exception.
+ db = new DisallowReadFromGroupsReviewDbWrapper(db);
+ }
+ return db;
}
}
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/SchemaCreator.java
index 9060b06..112fd55 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -16,6 +16,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.metrics.MetricMaker;
@@ -38,12 +39,15 @@
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.GroupConfig;
+import com.google.gerrit.server.group.db.GroupNameNotes;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupCreation;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
import com.google.gerrit.server.index.group.GroupIndex;
import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gwtorm.jdbc.JdbcExecutor;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -53,6 +57,7 @@
import java.nio.file.Path;
import java.util.Collections;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -68,7 +73,7 @@
private final PersonIdent serverUser;
private final DataSourceType dataSourceType;
private final GroupIndexCollection indexCollection;
- private final boolean writeGroupsToNoteDb;
+ private final GroupsMigration groupsMigration;
private final Config config;
private final MetricMaker metricMaker;
@@ -85,6 +90,7 @@
@GerritPersonIdent PersonIdent au,
DataSourceType dst,
GroupIndexCollection ic,
+ GroupsMigration gm,
@GerritServerConfig Config config,
MetricMaker metricMaker,
NotesMigration migration,
@@ -98,6 +104,7 @@
au,
dst,
ic,
+ gm,
config,
metricMaker,
migration,
@@ -113,6 +120,7 @@
@GerritPersonIdent PersonIdent au,
DataSourceType dst,
GroupIndexCollection ic,
+ GroupsMigration gm,
Config config,
MetricMaker metricMaker,
NotesMigration migration,
@@ -125,11 +133,7 @@
serverUser = au;
dataSourceType = dst;
indexCollection = ic;
- // TODO(aliceks): Remove this flag when all other necessary TODOs for writing groups to NoteDb
- // have been addressed.
- // Don't flip this flag in a production setting! We only added it to spread the implementation
- // of groups in NoteDb among several changes which are gradually merged.
- writeGroupsToNoteDb = config.getBoolean("user", null, "writeGroupsToNoteDb", false);
+ groupsMigration = gm;
this.config = config;
this.allProjectsName = apName;
@@ -166,28 +170,28 @@
allProjectsName,
allUsersName,
metricMaker);
- try (Repository repository = repoManager.openRepository(allUsersName)) {
- createAdminsGroup(db, seqs, repository, admins);
- createBatchUsersGroup(db, seqs, repository, batchUsers, admins.getUUID());
+ try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+ createAdminsGroup(db, seqs, allUsersRepo, admins);
+ createBatchUsersGroup(db, seqs, allUsersRepo, batchUsers, admins.getUUID());
}
dataSourceType.getIndexScript().run(db);
}
private void createAdminsGroup(
- ReviewDb db, Sequences seqs, Repository repository, GroupReference groupReference)
+ ReviewDb db, Sequences seqs, Repository allUsersRepo, GroupReference groupReference)
throws OrmException, IOException, ConfigInvalidException {
InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
InternalGroupUpdate groupUpdate =
InternalGroupUpdate.builder().setDescription("Gerrit Site Administrators").build();
- createGroup(db, repository, groupCreation, groupUpdate);
+ createGroup(db, allUsersRepo, groupCreation, groupUpdate);
}
private void createBatchUsersGroup(
ReviewDb db,
Sequences seqs,
- Repository repository,
+ Repository allUsersRepo,
GroupReference groupReference,
AccountGroup.UUID adminsGroupUuid)
throws OrmException, IOException, ConfigInvalidException {
@@ -198,23 +202,23 @@
.setOwnerGroupUUID(adminsGroupUuid)
.build();
- createGroup(db, repository, groupCreation, groupUpdate);
+ createGroup(db, allUsersRepo, groupCreation, groupUpdate);
}
private void createGroup(
ReviewDb db,
- Repository repository,
+ Repository allUsersRepo,
InternalGroupCreation groupCreation,
InternalGroupUpdate groupUpdate)
throws OrmException, ConfigInvalidException, IOException {
InternalGroup groupInReviewDb = createGroupInReviewDb(db, groupCreation, groupUpdate);
- if (!writeGroupsToNoteDb) {
+ if (!groupsMigration.writeToNoteDb()) {
index(groupInReviewDb);
return;
}
- InternalGroup createdGroup = createGroupInNoteDb(repository, groupCreation, groupUpdate);
+ InternalGroup createdGroup = createGroupInNoteDb(allUsersRepo, groupCreation, groupUpdate);
index(createdGroup);
}
@@ -228,23 +232,43 @@
}
private InternalGroup createGroupInNoteDb(
- Repository repository, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
+ Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws ConfigInvalidException, IOException, OrmDuplicateKeyException {
- GroupConfig groupConfig = GroupConfig.createForNewGroup(repository, groupCreation);
+ GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersRepo, groupCreation);
// We don't add any initial members or subgroups and hence the provided functions should never
// be called. To be on the safe side, we specify some valid functions.
groupConfig.setGroupUpdate(groupUpdate, Account.Id::toString, AccountGroup.UUID::get);
- try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(repository)) {
- groupConfig.commit(metaDataUpdate);
- }
+
+ AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey);
+ GroupNameNotes groupNameNotes =
+ GroupNameNotes.loadForNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName);
+
+ commit(allUsersRepo, groupConfig, groupNameNotes);
+
return groupConfig
.getLoadedGroup()
.orElseThrow(() -> new IllegalStateException("Created group wasn't automatically loaded"));
}
- private MetaDataUpdate createMetaDataUpdate(Repository repository) {
+ private void commit(
+ Repository allUsersRepo, GroupConfig groupConfig, GroupNameNotes groupNameNotes)
+ throws IOException {
+ BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
+ try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
+ groupConfig.commit(metaDataUpdate);
+ }
+ // MetaDataUpdates unfortunately can't be reused. -> Create a new one.
+ try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
+ groupNameNotes.commit(metaDataUpdate);
+ }
+ RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
+ }
+
+ private MetaDataUpdate createMetaDataUpdate(
+ Repository allUsersRepo, @Nullable BatchRefUpdate batchRefUpdate) {
MetaDataUpdate metaDataUpdate =
- new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repository);
+ new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo, batchRefUpdate);
metaDataUpdate.getCommitBuilder().setAuthor(serverUser);
metaDataUpdate.getCommitBuilder().setCommitter(serverUser);
return metaDataUpdate;
diff --git a/java/com/google/gerrit/server/schema/SchemaVersion.java b/java/com/google/gerrit/server/schema/SchemaVersion.java
index 2cb57a8..780b70a 100644
--- a/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/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_163> C = Schema_163.class;
+ public static final Class<Schema_164> C = Schema_164.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/java/com/google/gerrit/server/schema/Schema_164.java b/java/com/google/gerrit/server/schema/Schema_164.java
new file mode 100644
index 0000000..fca0ae8
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_164.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.schema.AclUtil.grant;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+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.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.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.SQLException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Grant read on group branches */
+public class Schema_164 extends SchemaVersion {
+ private static final String COMMIT_MSG = "Grant read permissions on group branches";
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final SystemGroupBackend systemGroupBackend;
+ private final PersonIdent serverUser;
+
+ @Inject
+ Schema_164(
+ Provider<Schema_163> prior,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ SystemGroupBackend systemGroupBackend,
+ @GerritPersonIdent PersonIdent serverUser) {
+ super(prior);
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.systemGroupBackend = systemGroupBackend;
+ this.serverUser = serverUser;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+ try (Repository git = repoManager.openRepository(allUsersName);
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ md.setMessage(COMMIT_MSG);
+
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection groups = config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
+ grant(
+ config,
+ groups,
+ Permission.READ,
+ false,
+ true,
+ systemGroupBackend.getGroup(REGISTERED_USERS));
+ config.commit(md);
+ } catch (IOException | ConfigInvalidException e) {
+ throw new OrmException("Failed to grant read permissions on group branches", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/update/RefUpdateUtil.java b/java/com/google/gerrit/server/update/RefUpdateUtil.java
index 17c7812..166f288 100644
--- a/java/com/google/gerrit/server/update/RefUpdateUtil.java
+++ b/java/com/google/gerrit/server/update/RefUpdateUtil.java
@@ -20,6 +20,7 @@
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -28,7 +29,25 @@
/**
* Execute a batch ref update, throwing a checked exception if not all updates succeeded.
*
+ * <p>Creates a new {@link RevWalk} used only for this operation.
+ *
* @param bru batch update; should already have been executed.
+ * @param repo repository that created {@code bru}.
+ * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
+ * #checkResults(BatchRefUpdate)} for details.
+ * @throws IOException if any result was not {@code OK}.
+ */
+ public static void executeChecked(BatchRefUpdate bru, Repository repo) throws IOException {
+ try (RevWalk rw = new RevWalk(repo)) {
+ executeChecked(bru, rw);
+ }
+ }
+
+ /**
+ * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
+ *
+ * @param bru batch update; should already have been executed.
+ * @param rw walk for executing the update.
* @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
* #checkResults(BatchRefUpdate)} for details.
* @throws IOException if any result was not {@code OK}.
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index 963a71a..2659831 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -102,7 +102,7 @@
synchronized Context subContext(SshSession newSession, String newCommandLine) {
Context ctx = new Context(this, newSession, newCommandLine);
- cleanup.add(ctx.cleanup);
+ ctx.cleanup.add(cleanup);
return ctx;
}
}
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index e8d20d0..8e13b61 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -23,6 +23,7 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
+ "//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
"//lib:gwtorm",
"//lib:h2",
diff --git a/java/com/google/gerrit/testing/GroupNoteDbMode.java b/java/com/google/gerrit/testing/GroupNoteDbMode.java
new file mode 100644
index 0000000..86e92b8
--- /dev/null
+++ b/java/com/google/gerrit/testing/GroupNoteDbMode.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.notedb.GroupsMigration;
+
+public enum GroupNoteDbMode {
+ /** NoteDb is disabled, groups are only in ReviewDb */
+ OFF(new GroupsMigration(false, false, false)),
+
+ /** Writing new groups to NoteDb is enabled. */
+ WRITE(new GroupsMigration(true, false, false)),
+
+ /**
+ * Reading/writing groups from/to NoteDb is enabled. Trying to read groups from ReviewDb throws an
+ * exception.
+ */
+ READ_WRITE(new GroupsMigration(true, true, false)),
+
+ /**
+ * All group tables in ReviewDb are entirely disabled. Trying to read groups from ReviewDb throws
+ * an exception. Reading groups through an unwrapped ReviewDb instance writing groups to ReviewDb
+ * is a No-Op.
+ */
+ ON(new GroupsMigration(true, true, true));
+
+ private static final String ENV_VAR = "GERRIT_NOTEDB_GROUPS";
+ private static final String SYS_PROP = "gerrit.notedb.groups";
+
+ public static GroupNoteDbMode get() {
+ String value = System.getenv(ENV_VAR);
+ if (Strings.isNullOrEmpty(value)) {
+ value = System.getProperty(SYS_PROP);
+ }
+ if (Strings.isNullOrEmpty(value)) {
+ return OFF;
+ }
+ value = value.toUpperCase().replace("-", "_");
+ GroupNoteDbMode mode = Enums.getIfPresent(GroupNoteDbMode.class, value).orNull();
+ if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
+ checkArgument(
+ mode != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
+ } else {
+ checkArgument(
+ mode != null,
+ "Invalid value for system property %s: %s",
+ SYS_PROP,
+ System.getProperty(SYS_PROP));
+ }
+ return mode;
+ }
+
+ private final GroupsMigration groupsMigration;
+
+ private GroupNoteDbMode(GroupsMigration groupsMigration) {
+ this.groupsMigration = groupsMigration;
+ }
+
+ public GroupsMigration getGroupsMigration() {
+ return groupsMigration;
+ }
+}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b5c2187..72629c7 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -29,6 +29,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllProjectsNameProvider;
@@ -161,6 +162,7 @@
});
bind(MetricMaker.class).to(DisabledMetricMaker.class);
install(cfgInjector.getInstance(GerritGlobalModule.class));
+ install(new GerritApiModule());
install(new DefaultPermissionBackendModule());
install(new SearchingChangeCacheImpl.Module());
factory(GarbageCollection.Factory.class);
diff --git a/java/gerrit/PRED_commit_edits_2.java b/java/gerrit/PRED_commit_edits_2.java
index 0490429..c196026 100644
--- a/java/gerrit/PRED_commit_edits_2.java
+++ b/java/gerrit/PRED_commit_edits_2.java
@@ -96,7 +96,10 @@
if (fileRegex.matcher(newName).find()
|| (oldName != null && fileRegex.matcher(oldName).find())) {
- List<Edit> edits = entry.getEdits();
+ // This cast still seems to be needed on JDK 8 as workaround for:
+ // https://bugs.openjdk.java.net/browse/JDK-8039214
+ @SuppressWarnings("cast")
+ List<Edit> edits = (List<Edit>) entry.getEdits();
if (edits.isEmpty()) {
continue;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index ce4d77b..67c014d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -35,6 +35,7 @@
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -79,12 +80,12 @@
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.testing.TestKey;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.account.WatchConfig;
@@ -93,7 +94,8 @@
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.index.account.StalenessChecker;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gerrit.server.project.RefPattern;
@@ -103,6 +105,7 @@
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.name.Named;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -114,6 +117,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bouncycastle.bcpg.ArmoredOutputStream;
@@ -122,8 +126,12 @@
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
@@ -153,6 +161,8 @@
@Inject private ExternalIdsUpdate.User externalIdsUpdateFactory;
+ @Inject private ExternalIdsUpdate.ServerNoReindex externalIdsUpdateNoReindexFactory;
+
@Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
@Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
@@ -163,6 +173,14 @@
@Inject protected Emails emails;
+ @Inject private StalenessChecker stalenessChecker;
+
+ @Inject private AccountIndexer accountIndexer;
+
+ @Inject
+ @Named("accounts")
+ private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
+
private AccountIndexedCounter accountIndexedCounter;
private RegistrationHandle accountIndexEventCounterHandle;
private RefUpdateCounter refUpdateCounter;
@@ -918,11 +936,11 @@
@Test
public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
throws Exception {
- TestAccount foo = accountCreator.create(name("foo"));
+ TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
String userRef = RefNames.refsUsers(foo.id);
accountIndexedCounter.clear();
- TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
fetch(allUsersRepo, userRef + ":userRef");
allUsersRepo.reset("userRef");
@@ -934,7 +952,7 @@
pushFactory
.create(
db,
- admin.getIdent(),
+ foo.getIdent(),
allUsersRepo,
"Update account config",
AccountConfig.ACCOUNT_CONFIG,
@@ -1058,17 +1076,18 @@
}
@Test
+ @Sandboxed
public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestAccount foo = accountCreator.create(name("foo"));
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
String userRef = RefNames.refsUsers(foo.id);
accountIndexedCounter.clear();
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
- grantLabel("Code-Review", -2, 2, allUsers, userRef, false, adminGroup.getGroupUUID(), false);
- grant(allUsers, userRef, Permission.SUBMIT, false, adminGroup.getGroupUUID());
+ grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
+ grantLabel("Code-Review", -2, 2, allUsers, userRef, false, adminGroupUuid(), false);
+ grant(allUsers, userRef, Permission.SUBMIT, false, adminGroupUuid());
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, userRef + ":userRef");
@@ -1229,18 +1248,15 @@
@Test
public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
- TestAccount foo = accountCreator.create(name("foo"));
+ TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
String userRef = RefNames.refsUsers(foo.id);
String noEmail = "no.email";
accountsUpdate.create().update(foo.id, a -> a.setPreferredEmail(noEmail));
accountIndexedCounter.clear();
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
-
- TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
fetch(allUsersRepo, userRef + ":userRef");
allUsersRepo.reset("userRef");
@@ -1251,7 +1267,7 @@
pushFactory
.create(
db,
- admin.getIdent(),
+ foo.getIdent(),
allUsersRepo,
"Update account config",
AccountConfig.ACCOUNT_CONFIG,
@@ -1268,15 +1284,13 @@
@Test
public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
- TestAccount foo = accountCreator.create(name("foo"));
+ TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
String userRef = RefNames.refsUsers(foo.id);
accountIndexedCounter.clear();
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
+ grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
- TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
fetch(allUsersRepo, userRef + ":userRef");
allUsersRepo.reset("userRef");
@@ -1287,7 +1301,7 @@
pushFactory
.create(
db,
- admin.getIdent(),
+ foo.getIdent(),
allUsersRepo,
"Update account config",
AccountConfig.ACCOUNT_CONFIG,
@@ -1326,15 +1340,16 @@
}
@Test
+ @Sandboxed
public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestAccount foo = accountCreator.create(name("foo"));
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
String userRef = RefNames.refsUsers(foo.id);
accountIndexedCounter.clear();
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
+ grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, userRef + ":userRef");
@@ -1776,6 +1791,101 @@
assertGroups(newUser, ImmutableList.of(group));
}
+ @Test
+ public void defaultPermissionsOnUserBranches() throws Exception {
+ String userRef = RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}";
+ assertPermissions(
+ allUsers,
+ groupRef(REGISTERED_USERS),
+ userRef,
+ true,
+ Permission.READ,
+ Permission.PUSH,
+ Permission.SUBMIT);
+
+ // TODO(ekempin): This permission should also be exclusive
+ assertLabelPermission(
+ allUsers, groupRef(REGISTERED_USERS), userRef, false, "Code-Review", -2, 2);
+
+ assertPermissions(
+ allUsers,
+ adminGroupRef(),
+ RefNames.REFS_USERS_DEFAULT,
+ true,
+ Permission.READ,
+ Permission.PUSH,
+ Permission.CREATE);
+ }
+
+ @Test
+ public void stalenessChecker() throws Exception {
+ // Newly created account is not stale.
+ AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
+ Account.Id accountId = new Account.Id(accountInfo._accountId);
+ assertThat(stalenessChecker.isStale(accountId)).isFalse();
+
+ // Manually updating the user ref makes the index document stale.
+ String userRef = RefNames.refsUsers(accountId);
+ try (Repository repo = repoManager.openRepository(allUsers);
+ ObjectInserter oi = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
+
+ PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+ CommitBuilder cb = new CommitBuilder();
+ cb.setTreeId(commit.getTree());
+ cb.setCommitter(ident);
+ cb.setAuthor(ident);
+ cb.setMessage(commit.getFullMessage());
+ ObjectId emptyCommit = oi.insert(cb);
+ oi.flush();
+
+ RefUpdate updateRef = repo.updateRef(userRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(emptyCommit);
+ assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleAccountAndReindex(accountId);
+
+ // Manually inserting/updating/deleting an external ID of the user makes the index document
+ // stale.
+ ExternalIdsUpdate externalIdsUpdateNoReindex = externalIdsUpdateNoReindexFactory.create();
+ ExternalId.Key key = ExternalId.Key.create("foo", "foo");
+ externalIdsUpdateNoReindex.insert(ExternalId.create(key, accountId));
+ assertStaleAccountAndReindex(accountId);
+
+ externalIdsUpdateNoReindex.upsert(
+ ExternalId.createWithEmail(key, accountId, "foo@example.com"));
+ assertStaleAccountAndReindex(accountId);
+
+ externalIdsUpdateNoReindex.delete(accountId, key);
+ assertStaleAccountAndReindex(accountId);
+
+ // Manually delete account
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
+ RefUpdate updateRef = repo.updateRef(userRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(ObjectId.zeroId());
+ updateRef.setForceUpdate(true);
+ assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleAccountAndReindex(accountId);
+ }
+
+ private void assertStaleAccountAndReindex(Account.Id accountId) throws IOException {
+ // Evict account from cache to be sure that we use the index state for staleness checks. This
+ // has to happen directly on the accounts cache because AccountCacheImpl triggers a reindex for
+ // the account.
+ accountsCache.invalidate(accountId);
+ assertThat(stalenessChecker.isStale(accountId)).isTrue();
+
+ // Reindex fixes staleness
+ accountIndexer.index(accountId);
+ assertThat(stalenessChecker.isStale(accountId)).isFalse();
+ }
+
private void assertGroups(String user, List<String> expected) throws Exception {
List<String> actual =
gApi.accounts().id(user).getGroups().stream().map(g -> g.name).collect(toList());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index bfd4aff..d01b64e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1228,11 +1228,7 @@
// create hidden project that is only visible to administrators
Project.NameKey p = createProject("p");
ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(
- cfg,
- Permission.READ,
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(),
- "refs/*");
+ Util.allow(cfg, Permission.READ, adminGroupUuid(), "refs/*");
Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
saveProjectConfig(p, cfg);
@@ -1305,11 +1301,7 @@
// create hidden project that is only visible to administrators
Project.NameKey p = createProject("p");
ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(
- cfg,
- Permission.READ,
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(),
- "refs/*");
+ Util.allow(cfg, Permission.READ, adminGroupUuid(), "refs/*");
Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
saveProjectConfig(p, cfg);
@@ -1352,11 +1344,7 @@
// create hidden project that is only visible to administrators
Project.NameKey p = createProject("p");
ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(
- cfg,
- Permission.READ,
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(),
- "refs/*");
+ Util.allow(cfg, Permission.READ, adminGroupUuid(), "refs/*");
Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
saveProjectConfig(p, cfg);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java
new file mode 100644
index 0000000..a5d93f6
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java
@@ -0,0 +1,209 @@
+// 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.api.group;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.group.ServerInitiated;
+import com.google.gerrit.server.group.db.GroupBundle;
+import com.google.gerrit.server.group.db.GroupRebuilder;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gerrit.testing.TestTimeUtil.TempClockStep;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class GroupRebuilderIT extends AbstractDaemonTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ Config config = new Config();
+ // This test is explicitly testing the migration from ReviewDb to NoteDb, and handles reading
+ // from NoteDb manually. It should work regardless of the value of noteDb.groups.write, however.
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false);
+ return config;
+ }
+
+ @Inject @GerritServerId private String serverId;
+ @Inject @ServerInitiated private Provider<GroupsUpdate> groupsUpdate;
+ @Inject private GroupBundle.Factory bundleFactory;
+ @Inject private GroupRebuilder rebuilder;
+
+ @Before
+ public void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ }
+
+ @After
+ public void resetTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void basicGroupProperties() throws Exception {
+ GroupInfo createdGroup = gApi.groups().create(name("group")).get();
+ try (BlockReviewDbUpdatesForGroups ctx = new BlockReviewDbUpdatesForGroups()) {
+ GroupBundle reviewDbBundle =
+ bundleFactory.fromReviewDb(db, new AccountGroup.Id(createdGroup.groupId));
+ deleteGroupRefs(reviewDbBundle);
+
+ assertThat(rebuild(reviewDbBundle)).isEqualTo(reviewDbBundle.truncateToSecond());
+ }
+ }
+
+ @Test
+ public void logFormat() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ GroupInfo group1 = gApi.groups().create(name("group1")).get();
+ GroupInfo group2 = gApi.groups().create(name("group2")).get();
+
+ try (TempClockStep step = TestTimeUtil.freezeClock()) {
+ gApi.groups().id(group1.id).addMembers(user.id.toString(), user2.id.toString());
+ }
+
+ gApi.groups().id(group1.id).addGroups(group2.id);
+
+ try (BlockReviewDbUpdatesForGroups ctx = new BlockReviewDbUpdatesForGroups()) {
+ GroupBundle reviewDbBundle =
+ bundleFactory.fromReviewDb(db, new AccountGroup.Id(group1.groupId));
+ deleteGroupRefs(reviewDbBundle);
+
+ GroupBundle noteDbBundle = rebuild(reviewDbBundle);
+ assertThat(noteDbBundle).isEqualTo(reviewDbBundle.truncateToSecond());
+
+ ImmutableList<CommitInfo> log = log(group1);
+ assertThat(log).hasSize(4);
+
+ assertThat(log.get(0)).message().isEqualTo("Create group");
+ assertThat(log.get(0)).author().name().isEqualTo(serverIdent.get().getName());
+ assertThat(log.get(0)).author().email().isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
+ assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.get().getTimeZoneOffset());
+ assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
+
+ assertThat(log.get(1))
+ .message()
+ .isEqualTo("Update group\n\nAdd: Administrator <" + admin.id + "@" + serverId + ">");
+ assertThat(log.get(1)).author().name().isEqualTo(admin.fullName);
+ assertThat(log.get(1)).author().email().isEqualTo(admin.id + "@" + serverId);
+ assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
+
+ assertThat(log.get(2))
+ .message()
+ .isEqualTo(
+ "Update group\n"
+ + "\n"
+ + ("Add: User <" + user.id + "@" + serverId + ">\n")
+ + ("Add: User2 <" + user2.id + "@" + serverId + ">"));
+ assertThat(log.get(2)).author().name().isEqualTo(admin.fullName);
+ assertThat(log.get(2)).author().email().isEqualTo(admin.id + "@" + serverId);
+ assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
+
+ assertThat(log.get(3))
+ .message()
+ .isEqualTo("Update group\n\nAdd-group: " + group2.name + " <" + group2.id + ">");
+ assertThat(log.get(3)).author().name().isEqualTo(admin.fullName);
+ assertThat(log.get(3)).author().email().isEqualTo(admin.id + "@" + serverId);
+ assertThat(log.get(3)).committer().hasSameDateAs(log.get(3).author);
+ }
+ }
+
+ private void deleteGroupRefs(GroupBundle bundle) throws Exception {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ String refName = RefNames.refsGroups(bundle.uuid());
+ RefUpdate ru = repo.updateRef(refName);
+ ru.setForceUpdate(true);
+ Ref oldRef = repo.exactRef(refName);
+ if (oldRef == null) {
+ return;
+ }
+ ru.setExpectedOldObjectId(oldRef.getObjectId());
+ ru.setNewObjectId(ObjectId.zeroId());
+ assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ }
+
+ private GroupBundle rebuild(GroupBundle reviewDbBundle) throws Exception {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ rebuilder.rebuild(repo, reviewDbBundle, null);
+ return bundleFactory.fromNoteDb(repo, reviewDbBundle.uuid());
+ }
+ }
+
+ private ImmutableList<CommitInfo> log(GroupInfo g) throws Exception {
+ ImmutableList.Builder<CommitInfo> result = ImmutableList.builder();
+ List<Date> commitDates = new ArrayList<>();
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(new AccountGroup.UUID(g.id)));
+ if (ref != null) {
+ rw.sort(RevSort.REVERSE);
+ rw.setRetainBody(true);
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ for (RevCommit c : rw) {
+ result.add(CommitUtil.toCommitInfo(c));
+ commitDates.add(c.getCommitterIdent().getWhen());
+ }
+ }
+ }
+ assertThat(commitDates).named("commit timestamps for %s", result).isOrdered();
+ return result.build();
+ }
+
+ private class BlockReviewDbUpdatesForGroups implements AutoCloseable {
+ BlockReviewDbUpdatesForGroups() {
+ blockReviewDbUpdates(true);
+ }
+
+ @Override
+ public void close() throws Exception {
+ blockReviewDbUpdates(false);
+ }
+
+ private void blockReviewDbUpdates(boolean block) {
+ cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", block);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index ae3e635..a000d9e 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -22,8 +22,15 @@
import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
import static java.util.stream.Collectors.toList;
+import com.google.common.base.Throwables;
+import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -33,6 +40,7 @@
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -50,6 +58,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
@@ -59,20 +68,26 @@
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.index.group.StalenessChecker;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.name.Named;
import java.io.IOException;
import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -83,9 +98,12 @@
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
@NoHttpd
@@ -93,13 +111,36 @@
@ConfigSuite.Config
public static Config noteDbConfig() {
Config config = new Config();
- config.setBoolean("user", null, "writeGroupsToNoteDb", true);
- config.setBoolean("user", null, "readGroupsFromNoteDb", true);
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
+ return config;
+ }
+
+ @ConfigSuite.Config
+ public static Config disableReviewDb() {
+ Config config = noteDbConfig();
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
return config;
}
@Inject private Groups groups;
@Inject private GroupIncludeCache groupIncludeCache;
+ @Inject private StalenessChecker stalenessChecker;
+ @Inject private GroupIndexer groupIndexer;
+
+ @Inject
+ @Named("groups_byuuid")
+ private LoadingCache<String, Optional<InternalGroup>> groupsByUUIDCache;
+
+ @Before
+ public void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ }
+
+ @After
+ public void resetTime() {
+ TestTimeUtil.useSystemTime();
+ }
@Test
public void systemGroupCanBeRetrievedFromIndex() throws Exception {
@@ -220,7 +261,7 @@
public void createGroup() throws Exception {
String newGroupName = name("newGroup");
GroupInfo g = gApi.groups().create(newGroupName).get();
- assertGroupInfo(getFromCache(newGroupName), g);
+ assertGroupInfo(group(newGroupName), g);
}
@Test
@@ -280,7 +321,7 @@
in.name = name("newGroup");
in.description = "Test description";
in.visibleToAll = true;
- in.ownerId = getFromCache("Administrators").getGroupUUID().get();
+ in.ownerId = adminGroupUuid().get();
GroupInfo g = gApi.groups().create(in).detail();
assertThat(g.description).isEqualTo(in.description);
assertThat(g.options.visibleToAll).isEqualTo(in.visibleToAll);
@@ -297,7 +338,7 @@
@Test
public void createdOnFieldIsPopulatedForNewGroup() throws Exception {
// NoteDb allows only second precision.
- Timestamp testStartTime = Timestamp.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
+ Timestamp testStartTime = TimeUtil.truncateToSecond(TimeUtil.nowTs());
String newGroupName = name("newGroup");
GroupInfo group = gApi.groups().create(newGroupName).get();
@@ -306,7 +347,7 @@
@Test
public void getGroup() throws Exception {
- InternalGroup adminGroup = getFromCache("Administrators");
+ InternalGroup adminGroup = adminGroup();
testGetGroup(adminGroup.getGroupUUID().get(), adminGroup);
testGetGroup(adminGroup.getName(), adminGroup);
testGetGroup(adminGroup.getId().get(), adminGroup);
@@ -346,40 +387,91 @@
}
@Test
- public void groupName() throws Exception {
- String name = name("group");
+ public void groupIsCreatedForSpecifiedName() throws Exception {
+ String name = name("Users");
gApi.groups().create(name);
- // get name
assertThat(gApi.groups().id(name).name()).isEqualTo(name);
-
- // set name to same name
- gApi.groups().id(name).name(name);
- assertThat(gApi.groups().id(name).name()).isEqualTo(name);
-
- // set name with name conflict
- String other = name("other");
- gApi.groups().create(other);
- exception.expect(ResourceConflictException.class);
- gApi.groups().id(name).name(other);
}
@Test
- public void groupRename() throws Exception {
- String name = name("group");
+ public void groupCannotBeCreatedWithNameOfAnotherGroup() throws Exception {
+ String name = name("Users");
+ gApi.groups().create(name).get();
+
+ exception.expect(ResourceConflictException.class);
gApi.groups().create(name);
+ }
- String newName = name("newName");
+ @Test
+ public void groupCanBeRenamed() throws Exception {
+ String name = name("Name1");
+ GroupInfo group = gApi.groups().create(name).get();
+
+ String newName = name("Name2");
gApi.groups().id(name).name(newName);
- assertThat(getFromCache(newName)).isNotNull();
- assertThat(gApi.groups().id(newName).name()).isEqualTo(newName);
+ assertThat(gApi.groups().id(group.id).name()).isEqualTo(newName);
+ }
- assertThat(getFromCache(name)).isNull();
+ @Test
+ public void groupCanBeRenamedToItsCurrentName() throws Exception {
+ String name = name("Users");
+ GroupInfo group = gApi.groups().create(name).get();
+
+ gApi.groups().id(group.id).name(name);
+ assertThat(gApi.groups().id(group.id).name()).isEqualTo(name);
+ }
+
+ @Test
+ public void groupCannotBeRenamedToNameOfAnotherGroup() throws Exception {
+ String name1 = name("Name1");
+ GroupInfo group1 = gApi.groups().create(name1).get();
+
+ String name2 = name("Name2");
+ gApi.groups().create(name2);
+
+ exception.expect(ResourceConflictException.class);
+ gApi.groups().id(group1.id).name(name2);
+ }
+
+ @Test
+ public void renamedGroupCanBeLookedUpByNewName() throws Exception {
+ String name = name("Name1");
+ GroupInfo group = gApi.groups().create(name).get();
+
+ String newName = name("Name2");
+ gApi.groups().id(group.id).name(newName);
+
+ GroupInfo foundGroup = gApi.groups().id(newName).get();
+ assertThat(foundGroup.id).isEqualTo(group.id);
+ }
+
+ @Test
+ public void oldNameOfRenamedGroupIsNotAccessibleAnymore() throws Exception {
+ String name = name("Name1");
+ GroupInfo group = gApi.groups().create(name).get();
+
+ String newName = name("Name2");
+ gApi.groups().id(group.id).name(newName);
+
+ assertGroupDoesNotExist(name);
exception.expect(ResourceNotFoundException.class);
gApi.groups().id(name).get();
}
@Test
+ public void oldNameOfRenamedGroupIsFreeForUseAgain() throws Exception {
+ String name = name("Name1");
+ GroupInfo group1 = gApi.groups().create(name).get();
+
+ String newName = name("Name2");
+ gApi.groups().id(group1.id).name(newName);
+
+ GroupInfo group2 = gApi.groups().create(name).get();
+ assertThat(group2.id).isNotEqualTo(group1.id);
+ }
+
+ @Test
public void groupDescription() throws Exception {
String name = name("group");
gApi.groups().create(name);
@@ -420,7 +512,7 @@
public void groupOwner() throws Exception {
String name = name("group");
GroupInfo info = gApi.groups().create(name).get();
- String adminUUID = getFromCache("Administrators").getGroupUUID().get();
+ String adminUUID = adminGroupUuid().get();
String registeredUUID = SystemGroupBackend.REGISTERED_USERS.get();
// get owner
@@ -579,7 +671,7 @@
@Test
public void listAllGroups() throws Exception {
List<String> expectedGroups =
- groups.getAll(db).map(InternalGroup::getName).sorted().collect(toList());
+ groups.getAllGroupReferences(db).map(GroupReference::getName).sorted().collect(toList());
assertThat(expectedGroups.size()).isAtLeast(2);
assertThat(gApi.groups().list().getAsMap().keySet())
.containsExactlyElementsIn(expectedGroups)
@@ -593,8 +685,7 @@
Arrays.asList(createGroup("test-child1", parent), createGroup("test-child2", parent));
// By UUID
- List<GroupInfo> owned =
- gApi.groups().list().withOwnedBy(getFromCache(parent).getGroupUUID().get()).get();
+ List<GroupInfo> owned = gApi.groups().list().withOwnedBy(groupUuid(parent).get()).get();
assertThat(owned.stream().map(g -> g.name).collect(toList()))
.containsExactlyElementsIn(children);
@@ -620,7 +711,7 @@
in.name = newGroupName;
in.description = "a hidden group";
in.visibleToAll = false;
- in.ownerId = getFromCache("Administrators").getGroupUUID().get();
+ in.ownerId = adminGroupUuid().get();
gApi.groups().create(in);
setApiUser(user);
@@ -668,7 +759,7 @@
assertThat(groups).containsKey(group);
assertThat(groups).containsKey(otherGroup);
- groups = gApi.groups().list().withSubstring("foo").getAsMap();
+ groups = gApi.groups().list().withSubstring("non-existing-substring").getAsMap();
assertThat(groups).isEmpty();
}
@@ -690,7 +781,7 @@
@Test
public void allGroupInfoFieldsSetCorrectly() throws Exception {
- InternalGroup adminGroup = getFromCache("Administrators");
+ InternalGroup adminGroup = adminGroup();
Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap();
assertThat(groups).hasSize(1);
assertThat(groups).containsKey("Administrators");
@@ -699,7 +790,6 @@
@Test
public void getAuditLog() throws Exception {
- assume().that(cfg.getBoolean("user", null, "readGroupsFromNoteDb", false)).isFalse();
GroupApi g = gApi.groups().create(name("group"));
List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(1);
@@ -727,10 +817,22 @@
assertThat(auditEvents).hasSize(5);
assertAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
+ // Add a removed member back again.
+ g.addMembers(user.username);
+ auditEvents = g.auditLog();
+ assertThat(auditEvents).hasSize(6);
+ assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+
+ // Add a removed group back again.
+ g.addGroups(otherGroup);
+ auditEvents = g.auditLog();
+ assertThat(auditEvents).hasSize(7);
+ assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+
Timestamp lastDate = null;
for (GroupAuditEventInfo auditEvent : auditEvents) {
if (lastDate != null) {
- assertThat(lastDate).isGreaterThan(auditEvent.date);
+ assertThat(lastDate).isAtLeast(auditEvent.date);
}
lastDate = auditEvent.date;
}
@@ -764,44 +866,57 @@
@Test
public void pushToGroupBranchIsRejectedForAllUsersRepo() throws Exception {
- pushToGroupBranch(allUsers, "Not allowed to create group branch.", "group update not allowed");
+ String groupRef =
+ RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
+ assertPushToGroupBranch(allUsers, groupRef, !groupsInNoteDb(), "group update not allowed");
}
@Test
- public void pushToGroupBranchForNonAllUsersRepo() throws Exception {
- pushToGroupBranch(project, null, null);
+ public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
+ assume().that(groupsInNoteDb()).isTrue(); // branch only exists when groups are in NoteDb
+ // refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, false, "group update not allowed");
}
- private void pushToGroupBranch(
- Project.NameKey project, String expectedErrorOnCreate, String expectedErrorOnUpdate)
+ @Test
+ public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
+ assertCreateGroupBranch(project, null);
+ String groupRef =
+ RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
+ assertPushToGroupBranch(project, groupRef, true, null);
+ }
+
+ @Test
+ public void pushToGroupNamesBranchForNonAllUsersRepo() throws Exception {
+ assertPushToGroupBranch(project, RefNames.REFS_GROUPNAMES, true, null);
+ }
+
+ private void assertPushToGroupBranch(
+ Project.NameKey project, String groupRefName, boolean createRef, String expectedErrorOnUpdate)
throws Exception {
grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
+ grant(project, RefNames.REFS_GROUPNAMES, Permission.PUSH, false, REGISTERED_USERS);
TestRepository<InMemoryRepository> repo = cloneProject(project);
- // create new branch
- PushOneCommit.Result r =
- pushFactory
- .create(
- db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
- .setParents(ImmutableList.of())
- .to(RefNames.REFS_GROUPS + name("foo"));
- if (expectedErrorOnCreate != null) {
- r.assertErrorStatus(expectedErrorOnCreate);
- } else {
- r.assertOkStatus();
+ if (createRef) {
+ createGroupBranch(project, groupRefName);
}
// update existing branch
- String groupRefName = RefNames.REFS_GROUPS + name("bar");
- createGroupBranch(project, groupRefName);
fetch(repo, groupRefName + ":groupRef");
repo.reset("groupRef");
- r =
+ PushOneCommit.Result r =
pushFactory
.create(
- db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
+ db,
+ admin.getIdent(),
+ repo,
+ "Update group config",
+ GroupConfig.GROUP_CONFIG_FILE,
+ "some content")
.to(groupRefName);
if (expectedErrorOnUpdate != null) {
r.assertErrorStatus(expectedErrorOnUpdate);
@@ -810,6 +925,29 @@
}
}
+ private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate)
+ throws Exception {
+ grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
+ grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(
+ db,
+ admin.getIdent(),
+ repo,
+ "Update group config",
+ GroupConfig.GROUP_CONFIG_FILE,
+ "some content")
+ .setParents(ImmutableList.of())
+ .to(RefNames.REFS_GROUPS + name("bar"));
+ if (expectedErrorOnCreate != null) {
+ r.assertErrorStatus(expectedErrorOnCreate);
+ } else {
+ r.assertOkStatus();
+ }
+ }
+
@Test
public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Exception {
pushToGroupBranchForReviewAndSubmit(allUsers, "group update not allowed");
@@ -867,12 +1005,7 @@
assume().that(groupsInNoteDb()).isTrue();
grant(allUsers, RefNames.REFS_GROUPS + "*", Permission.DELETE, true, REGISTERED_USERS);
-
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- assertThat(adminGroup).isNotNull();
- String groupRef = RefNames.refsGroups(adminGroup.getGroupUUID());
-
+ String groupRef = RefNames.refsGroups(adminGroupUuid());
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
PushResult r = deleteRef(allUsersRepo, groupRef);
RemoteRefUpdate refUpdate = r.getRemoteUpdate(groupRef);
@@ -884,6 +1017,103 @@
}
}
+ @Test
+ public void defaultPermissionsOnGroupBranches() throws Exception {
+ assertPermissions(
+ allUsers, groupRef(REGISTERED_USERS), RefNames.REFS_GROUPS + "*", true, Permission.READ);
+ }
+
+ @Test
+ @Sandboxed
+ public void blockReviewDbUpdatesOnGroupCreation() throws Exception {
+ assume().that(groupsInNoteDb()).isFalse();
+ cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", true);
+ try {
+ gApi.groups().create(name("foo"));
+ fail("Expected RestApiException: Updates to groups in ReviewDb are blocked");
+ } catch (RestApiException e) {
+ assertWriteGroupToReviewDbBlockedException(e);
+ }
+ }
+
+ @Test
+ @Sandboxed
+ public void blockReviewDbUpdatesOnGroupUpdate() throws Exception {
+ assume().that(groupsInNoteDb()).isFalse();
+ String group1 = gApi.groups().create(name("foo")).get().id;
+ String group2 = gApi.groups().create(name("bar")).get().id;
+ cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", true);
+ try {
+ gApi.groups().id(group1).addGroups(group2);
+ fail("Expected RestApiException: Updates to groups in ReviewDb are blocked");
+ } catch (RestApiException e) {
+ assertWriteGroupToReviewDbBlockedException(e);
+ }
+ }
+
+ private void assertWriteGroupToReviewDbBlockedException(Exception e) throws Exception {
+ Throwable t = Throwables.getRootCause(e);
+ assertThat(t).isInstanceOf(OrmException.class);
+ assertThat(t.getMessage()).isEqualTo("Updates to groups in ReviewDb are blocked");
+ }
+
+ @Test
+ public void stalenessChecker() throws Exception {
+ assume().that(readGroupsFromNoteDb()).isTrue();
+
+ // Newly created group is not stale
+ GroupInfo groupInfo = gApi.groups().create(name("foo")).get();
+ AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupInfo.id);
+ assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
+
+ // Manual update makes index document stale
+ String groupRef = RefNames.refsGroups(groupUuid);
+ try (Repository repo = repoManager.openRepository(allUsers);
+ ObjectInserter oi = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
+
+ PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+ CommitBuilder cb = new CommitBuilder();
+ cb.setTreeId(commit.getTree());
+ cb.setCommitter(ident);
+ cb.setAuthor(ident);
+ cb.setMessage(commit.getFullMessage());
+ ObjectId emptyCommit = oi.insert(cb);
+ oi.flush();
+
+ RefUpdate updateRef = repo.updateRef(groupRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(emptyCommit);
+ assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleGroupAndReindex(groupUuid);
+
+ // Manually delete group
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
+ RefUpdate updateRef = repo.updateRef(groupRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(ObjectId.zeroId());
+ updateRef.setForceUpdate(true);
+ assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleGroupAndReindex(groupUuid);
+ }
+
+ private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
+ // Evict group from cache to be sure that we use the index state for staleness checks. This has
+ // to happen directly on the groupsByUUID cache because GroupsCacheImpl triggers a reindex for
+ // the group.
+ groupsByUUIDCache.invalidate(groupUuid.get());
+ assertThat(stalenessChecker.isStale(groupUuid)).isTrue();
+
+ // Reindex fixes staleness
+ groupIndexer.index(groupUuid);
+ assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
+ }
+
private void pushToGroupBranchForReviewAndSubmit(Project.NameKey project, String expectedError)
throws Exception {
grantLabel(
@@ -988,10 +1218,6 @@
assertThat(gApi.groups().id(group).includedGroups()).isEmpty();
}
- private InternalGroup getFromCache(String name) throws Exception {
- return groupCache.get(new AccountGroup.NameKey(name)).orElse(null);
- }
-
private void assertBadRequest(ListRequest req) throws Exception {
try {
req.get();
@@ -1002,7 +1228,10 @@
}
private boolean groupsInNoteDb() {
- return cfg.getBoolean("user", "writeGroupsToNoteDb", false)
- && cfg.getBoolean("user", "readGroupsFromNoteDb", false);
+ return cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false);
+ }
+
+ private boolean readGroupsFromNoteDb() {
+ return groupsInNoteDb() && cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 75b7466..384dd7d 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -25,7 +25,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
@@ -46,8 +45,7 @@
normalProject = createProject("normal");
secretProject = createProject("secret");
secretRefProject = createProject("secretRef");
- privilegedGroup =
- groupCache.get(new AccountGroup.NameKey(createGroup("privilegedGroup"))).orElse(null);
+ privilegedGroup = group(createGroup("privilegedGroup"));
privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
gApi.groups().id(privilegedGroup.getGroupUUID().get()).addMembers(privilegedUser.username);
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index a6dcc62..12fde07 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -17,25 +17,34 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.util.stream.Collectors.toList;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
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.server.Sequences;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.git.ProjectConfig;
@@ -45,14 +54,16 @@
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testing.NoteDbMode;
import com.google.gerrit.testing.TestChanges;
import com.google.inject.Inject;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.Predicate;
import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.LsRemoteCommand;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -70,6 +81,7 @@
@Inject private AllUsersName allUsersName;
private AccountGroup.UUID admins;
+ private AccountGroup.UUID nonInteractiveUsers;
private ChangeData c1;
private ChangeData c2;
@@ -82,20 +94,29 @@
@Before
public void setUp() throws Exception {
- admins = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID();
+ admins = adminGroupUuid();
+ nonInteractiveUsers = groupUuid("Non-Interactive Users");
setUpPermissions();
setUpChanges();
}
private void setUpPermissions() throws Exception {
- // Remove read permissions for all users besides admin. This method is
- // idempotent, so is safe to call on every test setup.
+ // Remove read permissions for all users besides admin. This method is idempotent, so is safe
+ // to call on every test setup.
ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
for (AccessSection sec : pc.getAccessSections()) {
sec.removePermission(Permission.READ);
}
Util.allow(pc, Permission.READ, admins, "refs/*");
saveProjectConfig(allProjects, pc);
+
+ // Remove all read permissions on All-Users. This method is idempotent, so is safe to call on
+ // every test setup.
+ pc = projectCache.checkedGet(allUsers).getConfig();
+ for (AccessSection sec : pc.getAccessSections()) {
+ sec.removePermission(Permission.READ);
+ }
+ saveProjectConfig(allUsers, pc);
}
private static String changeRefPrefix(Change.Id id) {
@@ -447,22 +468,123 @@
}
@Test
+ public void advertisedReferencesDontShowUserBranchWithoutRead() throws Exception {
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getUserRefs(git)).isEmpty();
+ }
+ }
+
+ @Test
+ public void advertisedReferencesOmitUserBranchesOfOtherUsers() throws Exception {
+ allow(allUsersName, RefNames.REFS_USERS + "*", Permission.READ, REGISTERED_USERS);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getUserRefs(git))
+ .containsExactly(RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id));
+ }
+ }
+
+ @Test
+ public void advertisedReferencesIncludeAllUserBranchesWithAccessDatabase() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ try {
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getUserRefs(git))
+ .containsExactly(
+ RefNames.REFS_USERS_SELF,
+ RefNames.refsUsers(user.id),
+ RefNames.refsUsers(admin.id));
+ }
+ } finally {
+ removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesDontShowGroupBranchToOwnerWithoutRead() throws Exception {
+ createSelfOwnedGroup("Foos", user);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git)).isEmpty();
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesOmitGroupBranchesOfNonOwnedGroups() throws Exception {
+ allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+ AccountGroup.UUID users = createGroup("Users", admins, user);
+ AccountGroup.UUID foos = createGroup("Foos", users);
+ AccountGroup.UUID bars = createSelfOwnedGroup("Bars", user);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git))
+ .containsExactly(RefNames.refsGroups(foos), RefNames.refsGroups(bars));
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesIncludeAllGroupBranchesWithAccessDatabase() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ AccountGroup.UUID users = createGroup("Users", admins);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git))
+ .containsExactly(
+ RefNames.refsGroups(admins),
+ RefNames.refsGroups(nonInteractiveUsers),
+ RefNames.refsGroups(users));
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesIncludeAllGroupBranchesForAdmins() throws Exception {
+ allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+ try {
+ AccountGroup.UUID users = createGroup("Users", admins);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git))
+ .containsExactly(
+ RefNames.refsGroups(admins),
+ RefNames.refsGroups(nonInteractiveUsers),
+ RefNames.refsGroups(users));
+ }
+ } finally {
+ removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesOmitNoteDbNotesBranches() throws Exception {
+ allow(allUsersName, RefNames.REFS + "*", Permission.READ, REGISTERED_USERS);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getRefs(git)).containsNoneOf(RefNames.REFS_EXTERNAL_IDS, RefNames.REFS_GROUPNAMES);
+ }
+ }
+
+ @Test
public void advertisedReferencesOmitPrivateChangesOfOtherUsers() throws Exception {
allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
TestRepository<?> userTestRepository = cloneProject(project, user);
try (Git git = userTestRepository.git()) {
- LsRemoteCommand lsRemoteCommand = git.lsRemote();
String change3RefName = c3.currentPatchSet().getRefName();
-
- List<String> initialRefNames =
- lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
+ assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
gApi.changes().id(c3.getId().get()).setPrivate(true, null);
-
- List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertThat(refNames).doesNotContain(change3RefName);
+ assertThat(getRefs(git)).doesNotContain(change3RefName);
}
}
@@ -472,21 +594,16 @@
TestRepository<?> userTestRepository = cloneProject(project, user);
try (Git git = userTestRepository.git()) {
- LsRemoteCommand lsRemoteCommand = git.lsRemote();
String change3RefName = c3.currentPatchSet().getRefName();
-
- List<String> initialRefNames =
- lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
+ assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
gApi.changes().id(c3.getId().get()).setPrivate(true, null);
-
- List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertThat(refNames).contains(change3RefName);
+ assertThat(getRefs(git)).contains(change3RefName);
}
}
@Test
+ @Sandboxed
public void advertisedReferencesOmitDraftCommentRefsOfOtherUsers() throws Exception {
assume().that(notesMigration.commitChangeWrites()).isTrue();
@@ -509,6 +626,7 @@
}
@Test
+ @Sandboxed
public void advertisedReferencesOmitStarredChangesRefsOfOtherUsers() throws Exception {
assume().that(notesMigration.commitChangeWrites()).isTrue();
@@ -526,6 +644,57 @@
assertThat(lsRemote(allUsersName, accountCreator.user2())).doesNotContain(starredChangesRef);
}
+ @Test
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void hideMetadata() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ try {
+ // create change
+ TestRepository<?> allUsersRepo = cloneProject(allUsers);
+ fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userRef");
+ allUsersRepo.reset("userRef");
+ PushOneCommit.Result mr =
+ pushFactory
+ .create(db, admin.getIdent(), allUsersRepo)
+ .to("refs/for/" + RefNames.REFS_USERS_SELF);
+ mr.assertOkStatus();
+
+ List<String> expectedNonMetaRefs =
+ ImmutableList.of(
+ RefNames.REFS_USERS_SELF,
+ RefNames.refsUsers(admin.id),
+ RefNames.refsUsers(user.id),
+ RefNames.REFS_EXTERNAL_IDS,
+ RefNames.REFS_GROUPNAMES,
+ RefNames.refsGroups(admins),
+ RefNames.refsGroups(nonInteractiveUsers),
+ RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS,
+ RefNames.REFS_SEQUENCES + Sequences.NAME_GROUPS,
+ RefNames.REFS_CONFIG);
+
+ List<String> expectedMetaRefs =
+ new ArrayList<>(ImmutableList.of(mr.getPatchSetId().toRefName()));
+ if (NoteDbMode.get() != NoteDbMode.OFF) {
+ expectedMetaRefs.add(changeRefPrefix(mr.getChange().getId()) + "meta");
+ }
+
+ List<String> expectedAllRefs = new ArrayList<>(expectedNonMetaRefs);
+ expectedAllRefs.addAll(expectedMetaRefs);
+
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Map<String, Ref> all = repo.getAllRefs();
+
+ VisibleRefFilter filter = refFilterFactory.create(projectCache.get(allUsers), repo);
+ assertThat(filter.filter(all, false).keySet()).containsExactlyElementsIn(expectedAllRefs);
+
+ assertThat(filter.setShowMetadata(false).filter(all, false).keySet())
+ .containsExactlyElementsIn(expectedNonMetaRefs);
+ }
+ } finally {
+ removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ }
+ }
+
private List<String> lsRemote(Project.NameKey p, TestAccount a) throws Exception {
TestRepository<?> testRepository = cloneProject(p, a);
try (Git git = testRepository.git()) {
@@ -533,6 +702,22 @@
}
}
+ private List<String> getRefs(Git git) throws Exception {
+ return getRefs(git, Predicates.alwaysTrue());
+ }
+
+ private List<String> getUserRefs(Git git) throws Exception {
+ return getRefs(git, RefNames::isRefsUsers);
+ }
+
+ private List<String> getGroupRefs(Git git) throws Exception {
+ return getRefs(git, RefNames::isRefsGroups);
+ }
+
+ private List<String> getRefs(Git git, Predicate<String> predicate) throws Exception {
+ return git.lsRemote().call().stream().map(Ref::getName).filter(predicate).collect(toList());
+ }
+
/**
* Assert that refs seen by a non-admin user match expected.
*
@@ -590,4 +775,20 @@
assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps).isNotNull();
return ObjectId.fromString(ps.getRevision().get());
}
+
+ private AccountGroup.UUID createSelfOwnedGroup(String name, TestAccount... members)
+ throws RestApiException {
+ return createGroup(name, null, members);
+ }
+
+ private AccountGroup.UUID createGroup(
+ String name, @Nullable AccountGroup.UUID ownerGroup, TestAccount... members)
+ throws RestApiException {
+ GroupInput groupInput = new GroupInput();
+ groupInput.name = name(name);
+ groupInput.ownerId = ownerGroup != null ? ownerGroup.get() : null;
+ groupInput.members =
+ Arrays.stream(members).map(m -> String.valueOf(m.id.get())).collect(toList());
+ return new AccountGroup.UUID(gApi.groups().create(groupInput).get().id);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 4cc15d6..15e6a35 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -273,6 +273,8 @@
@Test
public void pushToExternalIdsBranch() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -296,6 +298,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -312,6 +316,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId()
throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -327,6 +333,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -342,6 +350,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -380,6 +390,8 @@
private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index cb0d768ef..8ca520d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -56,9 +56,9 @@
@Before
public void setUp() throws Exception {
- group1 = group("users1");
- group2 = group("users2");
- group3 = group("users3");
+ group1 = newGroup("users1");
+ group2 = newGroup("users2");
+ group3 = newGroup("users3");
user1 = user("user1", "First1 Last1", group1);
user2 = user("user2", "First2 Last2", group2);
@@ -235,8 +235,8 @@
@GerritConfig(name = "addreviewer.maxAllowed", value = "2")
@GerritConfig(name = "addreviewer.maxWithoutConfirmation", value = "1")
public void suggestReviewersGroupSizeConsiderations() throws Exception {
- InternalGroup largeGroup = group("large");
- InternalGroup mediumGroup = group("medium");
+ InternalGroup largeGroup = newGroup("large");
+ InternalGroup mediumGroup = newGroup("medium");
// Both groups have Administrator as a member. Add two users to large
// group to push it past maxAllowed, and one to medium group to push it
@@ -425,9 +425,9 @@
return gApi.changes().id(changeId).suggestReviewers(query).withLimit(n).get();
}
- private InternalGroup group(String name) throws Exception {
+ private InternalGroup newGroup(String name) throws Exception {
GroupInfo group = createGroupFactory.create(name(name)).apply(TopLevelResource.INSTANCE, null);
- return groupCache.get(new AccountGroup.UUID(group.id)).orElse(null);
+ return group(new AccountGroup.UUID(group.id));
}
private TestAccount user(String name, String fullName, String emailName, InternalGroup... groups)
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index b586ab2..bc27fff 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -15,25 +15,20 @@
package com.google.gerrit.acceptance.rest.config;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.group.InternalGroup;
import org.junit.Test;
public class FlushCacheIT extends AbstractDaemonTest {
@Test
public void flushCache() throws Exception {
- InternalGroup group = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- assertWithMessage("Precondition: The group 'Administrators' was loaded by the group cache")
- .that(group)
- .isNotNull();
+ // access the admin group once so that it is loaded into the group cache
+ adminGroup();
RestResponse r = adminRestSession.get("/config/server/caches/groups_byname");
CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 6002acd..241e695 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -36,12 +36,10 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
import java.util.HashMap;
import java.util.Map;
@@ -396,14 +394,11 @@
@Test
public void addNonGlobalCapabilityToGlobalCapabilities() throws Exception {
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
-
ProjectAccessInput accessInput = newProjectAccessInput();
AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
PermissionInfo permissionInfo = newPermissionInfo();
- permissionInfo.rules.put(adminGroup.getGroupUUID().get(), null);
+ permissionInfo.rules.put(adminGroupUuid().get(), null);
accessSectionInfo.permissions.put(Permission.PUSH, permissionInfo);
accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
@@ -426,14 +421,11 @@
@Test
public void removeGlobalCapabilityAsAdmin() throws Exception {
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
-
ProjectAccessInput accessInput = newProjectAccessInput();
AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
PermissionInfo permissionInfo = newPermissionInfo();
- permissionInfo.rules.put(adminGroup.getGroupUUID().get(), null);
+ permissionInfo.rules.put(adminGroupUuid().get(), null);
accessSectionInfo.permissions.put(GlobalCapability.ACCESS_DATABASE, permissionInfo);
// Add and validate first as removing existing privileges such as
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index b150df3..48dc994 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -32,7 +32,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.group.InternalGroup;
import org.junit.Before;
import org.junit.Test;
@@ -103,16 +102,13 @@
}
@Test
- @GerritConfig(name = "user.writeGroupsToNoteDb", value = "true")
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
public void createGroupBranch_Conflict() throws Exception {
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- assertThat(adminGroup).isNotNull();
assertCreateFails(
new Branch.NameKey(allUsers, RefNames.refsGroups(new AccountGroup.UUID("foo"))),
- RefNames.refsGroups(adminGroup.getGroupUUID()),
+ RefNames.refsGroups(adminGroupUuid()),
ResourceConflictException.class,
"Not allowed to create group branch.");
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index b47b51a..f90a73b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -326,10 +326,6 @@
}
}
- private AccountGroup.UUID groupUuid(String groupName) {
- return groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null).getGroupUUID();
- }
-
private void assertHead(String projectName, String expectedRef) throws Exception {
try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName))) {
assertThat(repo.exactRef(Constants.HEAD).getTarget().getName()).isEqualTo(expectedRef);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 3abc581..5e1b0bf 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -29,10 +29,8 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.group.InternalGroup;
import org.junit.Before;
import org.junit.Test;
@@ -140,18 +138,14 @@
}
@Test
- @GerritConfig(name = "user.writeGroupsToNoteDb", value = "true")
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
public void deleteGroupBranch_Conflict() throws Exception {
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- assertThat(adminGroup).isNotNull();
-
exception.expect(ResourceConflictException.class);
exception.expectMessage("Not allowed to delete group branch.");
- branch(new Branch.NameKey(allUsers, RefNames.refsGroups(adminGroup.getGroupUUID()))).delete();
+ branch(new Branch.NameKey(allUsers, RefNames.refsGroups(adminGroupUuid()))).delete();
}
private void blockForcePush() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 2ade7f6..a9ea42e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -217,6 +217,11 @@
assertThatNameList(gApi.projects().list().withType(FilterType.ALL).get())
.containsExactly(allProjects, allUsers, project)
.inOrder();
+
+ // "All" boolean option causes hidden projects to be included
+ assertThatNameList(gApi.projects().list().withAll().get())
+ .containsExactly(allProjects, allUsers, project, hidden)
+ .inOrder();
}
private void assertBadRequest(ListRequest req) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
index adc0f20..90f20ef 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -454,15 +454,6 @@
}
@Test
- public void rebuildReviewDbRequiresNoteDbPrimary() throws Exception {
- Change.Id id = createChange().getChange().getId();
-
- exception.expect(OrmException.class);
- exception.expectMessage("primary storage of " + id + " is REVIEW_DB");
- rebuilderWrapper.rebuildReviewDb(db, project, id);
- }
-
- @Test
public void migrateBackToReviewDbPrimary() throws Exception {
Change c = createChange().getChange().change();
Change.Id id = c.getId();
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index ca04a4a..50889ba 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -24,6 +24,7 @@
srcs = SERVER_TEST_SRCS,
deps = [
"//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/common:version",
"//java/com/google/gerrit/launcher",
"//lib:guava",
"//lib:truth",
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 3df89d9..9bb5246 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -40,7 +40,7 @@
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/testing:gerrit-test-util",
- "//javatests/com/google/gerrit/server:abstract_query_tests",
+ "//javatests/com/google/gerrit/server/query/%s:abstract_query_tests" % name,
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/jgit/org.eclipse.jgit.junit:junit",
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index c37a8ec..d1d6611 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -110,7 +110,7 @@
}
static void deleteAllIndexes(ElasticNodeInfo nodeInfo) {
- nodeInfo.node.client().admin().indices().prepareDelete("_all").execute();
+ nodeInfo.node.client().admin().indices().prepareDelete("_all").execute().actionGet();
}
static class NodeInfo {
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index a7f2e09..2eaf4f5 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -1,30 +1,5 @@
load("//tools/bzl:junit.bzl", "junit_tests")
-TESTUTIL_DEPS = [
- "//java/com/google/gerrit/common:annotations",
- "//java/com/google/gerrit/common:server",
- "//java/com/google/gerrit/extensions:api",
- "//java/com/google/gerrit/gpg",
- "//java/com/google/gerrit/lifecycle",
- "//java/com/google/gerrit/metrics",
- "//java/com/google/gerrit/reviewdb:server",
- "//java/com/google/gerrit/server",
- "//java/com/google/gerrit/server:module",
- "//java/com/google/gerrit/server/cache/h2",
- "//java/com/google/gerrit/index",
- "//java/com/google/gerrit/lucene",
- "//lib:gwtorm",
- "//lib:h2",
- "//lib:truth",
- "//lib/guice:guice",
- "//lib/guice:guice-servlet",
- "//lib/jgit/org.eclipse.jgit:jgit",
- "//lib/jgit/org.eclipse.jgit.junit:junit",
- "//lib/log:api",
- "//lib/log:impl_log4j",
- "//lib/log:log4j",
-]
-
CUSTOM_TRUTH_SUBJECTS = glob([
"**/*Subject.java",
])
@@ -41,109 +16,42 @@
],
)
-PROLOG_TEST_CASE = [
- "rules/PrologTestCase.java",
-]
-
-PROLOG_TESTS = glob(
- ["rules/**/*.java"],
- exclude = PROLOG_TEST_CASE,
-)
-
-java_library(
- name = "prolog_test_case",
- testonly = 1,
- srcs = PROLOG_TEST_CASE,
- deps = [
- "//java/com/google/gerrit/common:server",
- "//java/com/google/gerrit/extensions:api",
- "//java/com/google/gerrit/server",
- "//java/com/google/gerrit/testing:gerrit-test-util",
- "//lib:guava",
- "//lib:junit",
- "//lib:truth",
- "//lib/guice",
- "//lib/prolog:runtime",
- ],
-)
-
-junit_tests(
- name = "prolog_tests",
- srcs = PROLOG_TESTS,
- resource_strip_prefix = "prologtests",
- resources = ["//prologtests:gerrit_common_test"],
- deps = TESTUTIL_DEPS + [
- "//java/com/google/gerrit/testing:gerrit-test-util",
- "//java/com/google/gerrit/server/project/testing:project-test-util",
- "//prolog:gerrit-prolog-common",
- ":prolog_test_case",
- "//lib/prolog:runtime",
- ],
-)
-
-ABSTRACT_QUERY_TESTS = glob(
- ["query/**/AbstractQuery*Test.java"],
-)
-
-LUCENE_QUERY_TESTS = {i: "query/" + i + "/LuceneQuery" + i.capitalize() + "sTest.java" for i in [
- "account",
- "change",
- "group",
- "project",
-]}
-
-java_library(
- name = "abstract_query_tests",
- testonly = 1,
- srcs = ABSTRACT_QUERY_TESTS,
- visibility = ["//visibility:public"],
- deps = TESTUTIL_DEPS + [
- "//java/com/google/gerrit/testing:gerrit-test-util",
- "//prolog:gerrit-prolog-common",
- ],
-)
-
-[junit_tests(
- name = "lucene_query_%ss_test" % name,
- size = "large",
- srcs = [src],
- visibility = ["//visibility:public"],
- deps = TESTUTIL_DEPS + [
- ":abstract_query_tests",
- "//java/com/google/gerrit/testing:gerrit-test-util",
- ],
-) for name, src in LUCENE_QUERY_TESTS.items()]
-
junit_tests(
name = "server_tests",
size = "large",
srcs = glob(
["**/*.java"],
- exclude = CUSTOM_TRUTH_SUBJECTS + PROLOG_TESTS + PROLOG_TEST_CASE + ABSTRACT_QUERY_TESTS + LUCENE_QUERY_TESTS.values(),
+ exclude = CUSTOM_TRUTH_SUBJECTS,
),
resource_strip_prefix = "resources",
resources = ["//resources/com/google/gerrit/server"],
visibility = ["//visibility:public"],
- deps = TESTUTIL_DEPS + [
+ runtime_deps = [
+ "//lib/bouncycastle:bcprov",
+ ],
+ deps = [
":custom-truth-subjects",
- "//java/com/google/gerrit/extensions/client/testing:client-test-util",
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+ "//java/com/google/gerrit/index",
"//java/com/google/gerrit/index:query_exception",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/metrics",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//java/org/eclipse/jgit:server",
- "//java/com/google/gerrit/extensions/common/testing:common-test-util",
- "//lib:args4j",
"//lib:grappa",
"//lib:gson",
- "//lib:guava",
"//lib:guava-retrying",
- "//lib:protobuf",
+ "//lib:gwtorm",
"//lib:truth-java8-extension",
- "//lib/bouncycastle:bcprov",
- "//lib/bouncycastle:bcpkix",
- "//lib/guice:guice-assistedinject",
- "//lib/prolog:runtime",
"//lib/commons:codec",
- "//prolog:gerrit-prolog-common",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/jgit/org.eclipse.jgit.junit:junit",
],
)
diff --git a/javatests/com/google/gerrit/server/account/GroupUUIDTest.java b/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
new file mode 100644
index 0000000..3b72b08
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/GroupUUIDTest.java
@@ -0,0 +1,32 @@
+// 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.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.junit.Test;
+
+public class GroupUUIDTest {
+ @Test
+ public void createdUuidsForSameInputShouldBeDifferent() {
+ String groupName = "Users";
+ PersonIdent personIdent = new PersonIdent("John", "john@example.com");
+ AccountGroup.UUID uuid1 = GroupUUID.make(groupName, personIdent);
+ AccountGroup.UUID uuid2 = GroupUUID.make(groupName, personIdent);
+ assertThat(uuid2).isNotEqualTo(uuid1);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java
index a2b681d..d765c5e 100644
--- a/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java
@@ -190,9 +190,7 @@
assertMyMetaData(d2.getRefName(), 0);
assertThat(bru.getCommands().stream().map(ReceiveCommand::getRefName))
.containsExactly("refs/meta/1", "refs/meta/2");
- try (RevWalk rw = new RevWalk(repo)) {
- RefUpdateUtil.executeChecked(bru, rw);
- }
+ RefUpdateUtil.executeChecked(bru, repo);
assertMyMetaData(d1.getRefName(), 4, "Increment conf.value by 1", "Increment conf.value by 3");
assertMyMetaData(
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
new file mode 100644
index 0000000..70d5bf6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -0,0 +1,143 @@
+// 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.group.db;
+
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.sql.Timestamp;
+import java.util.TimeZone;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+
+@Ignore
+public class AbstractGroupTest extends GerritBaseTests {
+ protected static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+ protected static final String SERVER_ID = "server-id";
+ protected static final String SERVER_NAME = "Gerrit Server";
+ protected static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
+ protected static final int SERVER_ACCOUNT_NUMBER = 100000;
+ protected static final int USER_ACCOUNT_NUMBER = 100001;
+
+ protected AllUsersName allUsersName;
+ protected InMemoryRepositoryManager repoManager;
+ protected Repository allUsersRepo;
+ protected Account.Id serverAccountId;
+ protected PersonIdent serverIdent;
+ protected Account.Id userId;
+ protected PersonIdent userIdent;
+
+ @Before
+ public void abstractGroupTestSetUp() throws Exception {
+ allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
+ repoManager = new InMemoryRepositoryManager();
+ allUsersRepo = repoManager.createRepository(allUsersName);
+ serverAccountId = new Account.Id(SERVER_ACCOUNT_NUMBER);
+ serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ userId = new Account.Id(USER_ACCOUNT_NUMBER);
+ userIdent = newPersonIdent(userId, serverIdent);
+ }
+
+ @After
+ public void abstractGroupTestTearDown() throws Exception {
+ allUsersRepo.close();
+ }
+
+ protected Timestamp getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
+ try (RevWalk rw = new RevWalk(allUsersRepo)) {
+ Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
+ return ref == null
+ ? null
+ : new Timestamp(rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().getTime());
+ }
+ }
+
+ protected void assertTipCommit(AccountGroup.UUID uuid, String expectedMessage) throws Exception {
+ try (RevWalk rw = new RevWalk(allUsersRepo)) {
+ Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
+ assertCommit(
+ CommitUtil.toCommitInfo(rw.parseCommit(ref.getObjectId()), rw),
+ expectedMessage,
+ getAccountName(userId),
+ getAccountEmail(userId));
+ }
+ }
+
+ protected static void assertServerCommit(CommitInfo commitInfo, String expectedMessage) {
+ assertCommit(commitInfo, expectedMessage, SERVER_NAME, SERVER_EMAIL);
+ }
+
+ protected static void assertCommit(
+ CommitInfo commitInfo, String expectedMessage, String expectedName, String expectedEmail) {
+ assertThat(commitInfo).message().isEqualTo(expectedMessage);
+ assertThat(commitInfo).author().name().isEqualTo(expectedName);
+ assertThat(commitInfo).author().email().isEqualTo(expectedEmail);
+
+ // Committer should always be the server, regardless of author.
+ assertThat(commitInfo).committer().name().isEqualTo(SERVER_NAME);
+ assertThat(commitInfo).committer().email().isEqualTo(SERVER_EMAIL);
+ assertThat(commitInfo).committer().date().isEqualTo(commitInfo.author.date);
+ assertThat(commitInfo).committer().tz().isEqualTo(commitInfo.author.tz);
+ }
+
+ protected MetaDataUpdate createMetaDataUpdate(PersonIdent authorIdent) {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo);
+ md.getCommitBuilder().setAuthor(authorIdent);
+ md.getCommitBuilder().setCommitter(serverIdent); // Committer is always the server identity.
+ return md;
+ }
+
+ protected static PersonIdent newPersonIdent() {
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ }
+
+ protected static PersonIdent newPersonIdent(Account.Id id, PersonIdent ident) {
+ return new PersonIdent(
+ getAccountName(id), getAccountEmail(id), ident.getWhen(), ident.getTimeZone());
+ }
+
+ protected static String getAccountNameEmail(Account.Id id) {
+ return String.format("%s <%s>", getAccountName(id), getAccountEmail(id));
+ }
+
+ protected static String getGroupName(AccountGroup.UUID uuid) {
+ return String.format("Group <%s>", uuid);
+ }
+
+ protected static String getAccountName(Account.Id id) {
+ return "Account " + id;
+ }
+
+ protected static String getAccountEmail(Account.Id id) {
+ return String.format("%s@%s", id, SERVER_ID);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
new file mode 100644
index 0000000..c985c46
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -0,0 +1,382 @@
+// 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.group.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+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.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.group.InternalGroup;
+import java.sql.Timestamp;
+import java.util.Set;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for {@link AuditLogReader}. */
+public final class AuditLogReaderTest extends AbstractGroupTest {
+
+ private AuditLogReader auditLogReader;
+
+ @Before
+ public void setUp() throws Exception {
+ auditLogReader = new AuditLogReader(SERVER_ID);
+ }
+
+ @Test
+ public void createGroupAsUserIdent() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ AccountGroupMemberAudit expAudit =
+ createExpMemberAudit(group.getId(), userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expAudit);
+ }
+
+ @Test
+ public void createGroupAsServerIdent() throws Exception {
+ InternalGroup group = createGroup(1, "test-group", serverIdent, null);
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, group.getGroupUUID())).hasSize(0);
+ }
+
+ @Test
+ public void addAndRemoveMember() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ AccountGroupMemberAudit expAudit1 =
+ createExpMemberAudit(group.getId(), userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expAudit1);
+
+ // User adds account 100002 to the group.
+ Account.Id id = new Account.Id(100002);
+ addMembers(uuid, ImmutableSet.of(id));
+
+ AccountGroupMemberAudit expAudit2 =
+ createExpMemberAudit(group.getId(), id, userId, getTipTimestamp(uuid));
+ assertTipCommit(uuid, "Update group\n\nAdd: Account 100002 <100002@server-id>");
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+ .containsExactly(expAudit1, expAudit2)
+ .inOrder();
+
+ // User removes account 100002 from the group.
+ removeMembers(uuid, ImmutableSet.of(id));
+ assertTipCommit(uuid, "Update group\n\nRemove: Account 100002 <100002@server-id>");
+
+ expAudit2.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+ .containsExactly(expAudit1, expAudit2)
+ .inOrder();
+ }
+
+ @Test
+ public void addMultiMembers() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.Id groupId = group.getId();
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ AccountGroupMemberAudit expAudit1 =
+ createExpMemberAudit(groupId, userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expAudit1);
+
+ Account.Id id1 = new Account.Id(100002);
+ Account.Id id2 = new Account.Id(100003);
+ addMembers(uuid, ImmutableSet.of(id1, id2));
+
+ AccountGroupMemberAudit expAudit2 =
+ createExpMemberAudit(groupId, id1, userId, getTipTimestamp(uuid));
+ AccountGroupMemberAudit expAudit3 =
+ createExpMemberAudit(groupId, id2, userId, getTipTimestamp(uuid));
+
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + "Add: Account 100002 <100002@server-id>\n"
+ + "Add: Account 100003 <100003@server-id>");
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+ .containsExactly(expAudit1, expAudit2, expAudit3)
+ .inOrder();
+ }
+
+ @Test
+ public void addAndRemoveSubgroups() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ InternalGroup subgroup = createGroupAsUser(2, "test-group-2");
+ AccountGroup.UUID subgroupUuid = subgroup.getGroupUUID();
+
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid));
+ assertTipCommit(uuid, String.format("Update group\n\nAdd-group: Group <%s>", subgroupUuid));
+
+ AccountGroupByIdAud expAudit =
+ createExpGroupAudit(group.getId(), subgroupUuid, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid)).containsExactly(expAudit);
+
+ removeSubgroups(uuid, ImmutableSet.of(subgroupUuid));
+ assertTipCommit(uuid, String.format("Update group\n\nRemove-group: Group <%s>", subgroupUuid));
+
+ expAudit.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid)).containsExactly(expAudit);
+ }
+
+ @Test
+ public void addMultiSubgroups() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ InternalGroup subgroup1 = createGroupAsUser(2, "test-group-2");
+ InternalGroup subgroup2 = createGroupAsUser(3, "test-group-3");
+ AccountGroup.UUID subgroupUuid1 = subgroup1.getGroupUUID();
+ AccountGroup.UUID subgroupUuid2 = subgroup2.getGroupUUID();
+
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid1, subgroupUuid2));
+
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add-group: Group <%s>\n", subgroupUuid1)
+ + String.format("Add-group: Group <%s>", subgroupUuid2));
+
+ AccountGroupByIdAud expAudit1 =
+ createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
+ AccountGroupByIdAud expAudit2 =
+ createExpGroupAudit(group.getId(), subgroupUuid2, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
+ .containsExactly(expAudit1, expAudit2)
+ .inOrder();
+ }
+
+ @Test
+ public void addAndRemoveMembersAndSubgroups() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.Id groupId = group.getId();
+ AccountGroup.UUID uuid = group.getGroupUUID();
+ AccountGroupMemberAudit expMemberAudit =
+ createExpMemberAudit(groupId, userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expMemberAudit);
+
+ Account.Id id1 = new Account.Id(100002);
+ Account.Id id2 = new Account.Id(100003);
+ Account.Id id3 = new Account.Id(100004);
+ InternalGroup subgroup1 = createGroupAsUser(2, "test-group-2");
+ InternalGroup subgroup2 = createGroupAsUser(3, "test-group-3");
+ InternalGroup subgroup3 = createGroupAsUser(4, "test-group-4");
+ AccountGroup.UUID subgroupUuid1 = subgroup1.getGroupUUID();
+ AccountGroup.UUID subgroupUuid2 = subgroup2.getGroupUUID();
+ AccountGroup.UUID subgroupUuid3 = subgroup3.getGroupUUID();
+
+ // Add two accounts.
+ addMembers(uuid, ImmutableSet.of(id1, id2));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add: Account %s <%s@server-id>\n", id1, id1)
+ + String.format("Add: Account %s <%s@server-id>", id2, id2));
+ AccountGroupMemberAudit expMemberAudit1 =
+ createExpMemberAudit(groupId, id1, userId, getTipTimestamp(uuid));
+ AccountGroupMemberAudit expMemberAudit2 =
+ createExpMemberAudit(groupId, id2, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+ .containsExactly(expMemberAudit, expMemberAudit1, expMemberAudit2)
+ .inOrder();
+
+ // Add one subgroup.
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid1));
+ assertTipCommit(uuid, String.format("Update group\n\nAdd-group: Group <%s>", subgroupUuid1));
+ AccountGroupByIdAud expGroupAudit1 =
+ createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
+ .containsExactly(expGroupAudit1);
+
+ // Remove one account.
+ removeMembers(uuid, ImmutableSet.of(id2));
+ assertTipCommit(
+ uuid, String.format("Update group\n\nRemove: Account %s <%s@server-id>", id2, id2));
+ expMemberAudit2.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+ .containsExactly(expMemberAudit, expMemberAudit1, expMemberAudit2)
+ .inOrder();
+
+ // Add two subgroups.
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid2, subgroupUuid3));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add-group: Group <%s>\n", subgroupUuid2)
+ + String.format("Add-group: Group <%s>", subgroupUuid3));
+ AccountGroupByIdAud expGroupAudit2 =
+ createExpGroupAudit(group.getId(), subgroupUuid2, userId, getTipTimestamp(uuid));
+ AccountGroupByIdAud expGroupAudit3 =
+ createExpGroupAudit(group.getId(), subgroupUuid3, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
+ .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3)
+ .inOrder();
+
+ // Add two account, including a removed account.
+ addMembers(uuid, ImmutableSet.of(id2, id3));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add: Account %s <%s@server-id>\n", id2, id2)
+ + String.format("Add: Account %s <%s@server-id>", id3, id3));
+ AccountGroupMemberAudit expMemberAudit4 =
+ createExpMemberAudit(groupId, id2, userId, getTipTimestamp(uuid));
+ AccountGroupMemberAudit expMemberAudit3 =
+ createExpMemberAudit(groupId, id3, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+ .containsExactly(
+ expMemberAudit, expMemberAudit1, expMemberAudit2, expMemberAudit4, expMemberAudit3)
+ .inOrder();
+
+ // Remove two subgroups.
+ removeSubgroups(uuid, ImmutableSet.of(subgroupUuid1, subgroupUuid3));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Remove-group: Group <%s>\n", subgroupUuid1)
+ + String.format("Remove-group: Group <%s>", subgroupUuid3));
+ expGroupAudit1.removed(userId, getTipTimestamp(uuid));
+ expGroupAudit3.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
+ .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3)
+ .inOrder();
+
+ // Add back one removed subgroup.
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid1));
+ AccountGroupByIdAud expGroupAudit4 =
+ createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(allUsersRepo, uuid))
+ .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3, expGroupAudit4)
+ .inOrder();
+ }
+
+ private InternalGroup createGroupAsUser(int next, String groupName) throws Exception {
+ return createGroup(next, groupName, userIdent, userId);
+ }
+
+ private InternalGroup createGroup(
+ int next, String groupName, PersonIdent authorIdent, Account.Id authorId) throws Exception {
+ InternalGroupCreation groupCreation =
+ InternalGroupCreation.builder()
+ .setGroupUUID(GroupUUID.make(groupName, serverIdent))
+ .setNameKey(new AccountGroup.NameKey(groupName))
+ .setId(new AccountGroup.Id(next))
+ .setCreatedOn(TimeUtil.nowTs())
+ .build();
+ InternalGroupUpdate groupUpdate =
+ authorIdent.equals(serverIdent)
+ ? InternalGroupUpdate.builder().setDescription("Groups").build()
+ : InternalGroupUpdate.builder()
+ .setDescription("Groups")
+ .setMemberModification(members -> ImmutableSet.of(authorId))
+ .build();
+
+ GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersRepo, groupCreation);
+ groupConfig.setGroupUpdate(
+ groupUpdate, AbstractGroupTest::getAccountNameEmail, AbstractGroupTest::getGroupName);
+
+ RevCommit commit = groupConfig.commit(createMetaDataUpdate(authorIdent));
+ assertCreateGroup(authorIdent, commit);
+ return groupConfig
+ .getLoadedGroup()
+ .orElseThrow(() -> new IllegalStateException("create group failed"));
+ }
+
+ private void assertCreateGroup(PersonIdent authorIdent, RevCommit commit) throws Exception {
+ if (authorIdent.equals(serverIdent)) {
+ assertServerCommit(CommitUtil.toCommitInfo(commit), "Create group");
+ } else {
+ assertCommit(
+ CommitUtil.toCommitInfo(commit),
+ String.format("Create group\n\nAdd: Account %s <%s@%s>", userId, userId, SERVER_ID),
+ getAccountName(userId),
+ getAccountEmail(userId));
+ }
+ }
+
+ private InternalGroup updateGroup(AccountGroup.UUID uuid, InternalGroupUpdate groupUpdate)
+ throws Exception {
+ GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, uuid);
+ groupConfig.setGroupUpdate(
+ groupUpdate, AbstractGroupTest::getAccountNameEmail, AbstractGroupTest::getGroupName);
+
+ groupConfig.commit(createMetaDataUpdate(userIdent));
+ return groupConfig
+ .getLoadedGroup()
+ .orElseThrow(() -> new IllegalStateException("updated group failed"));
+ }
+
+ private InternalGroup addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setMemberModification(memberIds -> Sets.union(memberIds, ids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private InternalGroup removeMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setMemberModification(memberIds -> Sets.difference(memberIds, ids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private InternalGroup addSubgroups(AccountGroup.UUID groupUuid, Set<AccountGroup.UUID> uuids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setSubgroupModification(memberIds -> Sets.union(memberIds, uuids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private InternalGroup removeSubgroups(AccountGroup.UUID groupUuid, Set<AccountGroup.UUID> uuids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setSubgroupModification(memberIds -> Sets.difference(memberIds, uuids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private AccountGroupMemberAudit createExpMemberAudit(
+ AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Timestamp addedOn) {
+ return new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(id, groupId, addedOn), addedBy);
+ }
+
+ private AccountGroupByIdAud createExpGroupAudit(
+ AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Timestamp addedOn) {
+ return new AccountGroupByIdAud(new AccountGroupByIdAud.Key(groupId, uuid, addedOn), addedBy);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
new file mode 100644
index 0000000..65e09f6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -0,0 +1,19 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "db_tests",
+ size = "small",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/group/db/testing",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:truth",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/jgit/org.eclipse.jgit.junit:junit",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
new file mode 100644
index 0000000..53bf866
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -0,0 +1,213 @@
+// 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.group.db;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.group.db.testing.GroupTestUtil;
+import com.google.gerrit.server.update.RefUpdateUtil;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.testing.TestTimeUtil;
+import java.util.Arrays;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GroupNameNotesTest extends GerritBaseTests {
+ private static final String SERVER_NAME = "Gerrit Server";
+ private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
+ private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+
+ private AtomicInteger idCounter;
+ private Repository repo;
+
+ @Before
+ public void setUp() {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ idCounter = new AtomicInteger();
+ repo = new InMemoryRepository(new DfsRepositoryDescription(AllUsersNameProvider.DEFAULT));
+ }
+
+ @After
+ public void tearDown() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void updateGroupNames() throws Exception {
+ GroupReference g1 = newGroup("a");
+ GroupReference g2 = newGroup("b");
+
+ PersonIdent ident = newPersonIdent();
+ updateGroupNames(ident, g1, g2);
+
+ ImmutableList<CommitInfo> log = log();
+ assertThat(log).hasSize(1);
+ assertThat(log.get(0)).parents().isEmpty();
+ assertThat(log.get(0)).message().isEqualTo("Store 2 group names");
+ assertThat(log.get(0)).author().matches(ident);
+ assertThat(log.get(0)).committer().matches(ident);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+
+ // Updating the same set of names is a no-op.
+ String commit = log.get(0).commit;
+ updateGroupNames(newPersonIdent(), g1, g2);
+ log = log();
+ assertThat(log).hasSize(1);
+ assertThat(log.get(0)).commit().isEqualTo(commit);
+ }
+
+ @Test
+ public void updateGroupNamesOverwritesExistingNotes() throws Exception {
+ GroupReference g1 = newGroup("a");
+ GroupReference g2 = newGroup("b");
+
+ TestRepository<?> tr = new TestRepository<>(repo);
+ ObjectId k1 = getNoteKey(g1);
+ ObjectId k2 = getNoteKey(g2);
+ ObjectId k3 = GroupNameNotes.getNoteKey(new AccountGroup.NameKey("c"));
+ PersonIdent ident = newPersonIdent();
+ ObjectId origCommitId =
+ tr.branch(REFS_GROUPNAMES)
+ .commit()
+ .message("Prepopulate group name")
+ .author(ident)
+ .committer(ident)
+ .add(k1.name(), "[group]\n\tuuid = a-1\n\tname = a\nanotherKey = foo\n")
+ .add(k2.name(), "[group]\n\tuuid = a-1\n\tname = b\n")
+ .add(k3.name(), "[group]\n\tuuid = c-3\n\tname = c\n")
+ .create()
+ .copy();
+
+ ident = newPersonIdent();
+ updateGroupNames(ident, g1, g2);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+
+ ImmutableList<CommitInfo> log = log();
+ assertThat(log).hasSize(2);
+ assertThat(log.get(0)).commit().isEqualTo(origCommitId.name());
+
+ assertThat(log.get(1)).message().isEqualTo("Store 2 group names");
+ assertThat(log.get(1)).author().matches(ident);
+ assertThat(log.get(1)).committer().matches(ident);
+
+ try (RevWalk rw = new RevWalk(repo)) {
+ ObjectReader reader = rw.getObjectReader();
+ NoteMap noteMap =
+ NoteMap.read(reader, rw.parseCommit(ObjectId.fromString(log.get(1).commit)));
+ String note = new String(reader.open(noteMap.get(k1), OBJ_BLOB).getCachedBytes(), UTF_8);
+ // Old note content was overwritten.
+ assertThat(note).isEqualTo("[group]\n\tuuid = a-1\n\tname = a\n");
+ }
+ }
+
+ @Test
+ public void updateGroupNamesWithEmptyCollectionClearsAllNotes() throws Exception {
+ GroupReference g1 = newGroup("a");
+ GroupReference g2 = newGroup("b");
+
+ PersonIdent ident = newPersonIdent();
+ updateGroupNames(ident, g1, g2);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+
+ updateGroupNames(ident);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).isEmpty();
+
+ ImmutableList<CommitInfo> log = log();
+ assertThat(log).hasSize(2);
+ assertThat(log.get(1)).message().isEqualTo("Store 0 group names");
+ }
+
+ @Test
+ public void updateGroupNamesRejectsNonOneToOneGroupReferences() throws Exception {
+ assertIllegalArgument(
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name2"));
+ assertIllegalArgument(
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
+ new GroupReference(new AccountGroup.UUID("uuid2"), "name1"));
+ assertIllegalArgument(
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"));
+ }
+
+ private GroupReference newGroup(String name) {
+ int id = idCounter.incrementAndGet();
+ return new GroupReference(new AccountGroup.UUID(name + "-" + id), name);
+ }
+
+ private static PersonIdent newPersonIdent() {
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ }
+
+ private static ObjectId getNoteKey(GroupReference g) {
+ return GroupNameNotes.getNoteKey(new AccountGroup.NameKey(g.getName()));
+ }
+
+ private void updateGroupNames(PersonIdent ident, GroupReference... groupRefs) throws Exception {
+ try (ObjectInserter inserter = repo.newObjectInserter()) {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ GroupNameNotes.updateGroupNames(repo, inserter, bru, Arrays.asList(groupRefs), ident);
+ inserter.flush();
+ RefUpdateUtil.executeChecked(bru, repo);
+ }
+ }
+
+ private void assertIllegalArgument(GroupReference... groupRefs) throws Exception {
+ try (ObjectInserter inserter = repo.newObjectInserter()) {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ PersonIdent ident = newPersonIdent();
+ try {
+ GroupNameNotes.updateGroupNames(repo, inserter, bru, Arrays.asList(groupRefs), ident);
+ assert_().fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
+ }
+ }
+ }
+
+ private ImmutableList<CommitInfo> log() throws Exception {
+ return GroupTestUtil.log(repo, REFS_GROUPNAMES);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
new file mode 100644
index 0000000..b142887
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
@@ -0,0 +1,476 @@
+// 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.group.db;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
+import static com.google.gerrit.server.group.db.GroupBundle.builder;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.group.db.testing.GroupTestUtil;
+import com.google.gerrit.server.update.RefUpdateUtil;
+import com.google.gerrit.testing.TestTimeUtil;
+import java.sql.Timestamp;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.IntStream;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GroupRebuilderTest extends AbstractGroupTest {
+ private AtomicInteger idCounter;
+ private Repository repo;
+ private GroupRebuilder rebuilder;
+ private GroupBundle.Factory bundleFactory;
+
+ @Before
+ public void setUp() throws Exception {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ idCounter = new AtomicInteger();
+ repo = repoManager.createRepository(allUsersName);
+ rebuilder =
+ new GroupRebuilder(
+ GroupRebuilderTest::newPersonIdent,
+ allUsersName,
+ (project, repo, batch) ->
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo, batch),
+ // Note that the expected name/email values in tests are not necessarily realistic,
+ // since they use these trivial name/email functions. GroupRebuilderIT checks the actual
+ // values.
+ AbstractGroupTest::newPersonIdent,
+ AbstractGroupTest::getAccountNameEmail,
+ AbstractGroupTest::getGroupName);
+ bundleFactory = new GroupBundle.Factory(new AuditLogReader(SERVER_ID));
+ }
+
+ @After
+ public void tearDown() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void minimalGroupFields() throws Exception {
+ AccountGroup g = newGroup("a");
+ GroupBundle b = builder().group(g).build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(1);
+ assertCommit(log.get(0), "Create group", SERVER_NAME, SERVER_EMAIL);
+ assertThat(logGroupNames()).isEmpty();
+ }
+
+ @Test
+ public void allGroupFields() throws Exception {
+ AccountGroup g = newGroup("a");
+ g.setDescription("Description");
+ g.setOwnerGroupUUID(new AccountGroup.UUID("owner"));
+ g.setVisibleToAll(true);
+ GroupBundle b = builder().group(g).build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(1);
+ assertServerCommit(log.get(0), "Create group");
+ }
+
+ @Test
+ public void membersAndSubgroups() throws Exception {
+ AccountGroup g = newGroup("a");
+ GroupBundle b =
+ builder()
+ .group(g)
+ .members(member(g, 1), member(g, 2))
+ .byId(byId(g, "x"), byId(g, "y"))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(2);
+ assertServerCommit(log.get(0), "Create group");
+ assertServerCommit(
+ log.get(1),
+ "Update group\n"
+ + "\n"
+ + "Add: Account 1 <1@server-id>\n"
+ + "Add: Account 2 <2@server-id>\n"
+ + "Add-group: Group <x>\n"
+ + "Add-group: Group <y>");
+ }
+
+ @Test
+ public void memberAudit() throws Exception {
+ AccountGroup g = newGroup("a");
+ Timestamp t1 = TimeUtil.nowTs();
+ Timestamp t2 = TimeUtil.nowTs();
+ Timestamp t3 = TimeUtil.nowTs();
+ GroupBundle b =
+ builder()
+ .group(g)
+ .members(member(g, 1))
+ .memberAudit(addMember(g, 1, 8, t2), addAndRemoveMember(g, 2, 8, t1, 9, t3))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(4);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(
+ log.get(1), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 8", "8@server-id");
+ assertCommit(
+ log.get(2), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
+ assertCommit(
+ log.get(3), "Update group\n\nRemove: Account 2 <2@server-id>", "Account 9", "9@server-id");
+ }
+
+ @Test
+ public void memberAuditLegacyRemoved() throws Exception {
+ AccountGroup g = newGroup("a");
+ GroupBundle b =
+ builder()
+ .group(g)
+ .members(member(g, 2))
+ .memberAudit(
+ addAndLegacyRemoveMember(g, 1, 8, TimeUtil.nowTs()),
+ addMember(g, 2, 8, TimeUtil.nowTs()))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(4);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(
+ log.get(1), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
+ assertCommit(
+ log.get(2), "Update group\n\nRemove: Account 1 <1@server-id>", "Account 8", "8@server-id");
+ assertCommit(
+ log.get(3), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 8", "8@server-id");
+ }
+
+ @Test
+ public void unauditedMembershipsAddedAtEnd() throws Exception {
+ AccountGroup g = newGroup("a");
+ GroupBundle b =
+ builder()
+ .group(g)
+ .members(member(g, 1), member(g, 2), member(g, 3))
+ .memberAudit(addMember(g, 1, 8, TimeUtil.nowTs()))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(3);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(
+ log.get(1), "Update group\n\nAdd: Account 1 <1@server-id>", "Account 8", "8@server-id");
+ assertServerCommit(
+ log.get(2), "Update group\n\nAdd: Account 2 <2@server-id>\nAdd: Account 3 <3@server-id>");
+ }
+
+ @Test
+ public void byIdAudit() throws Exception {
+ AccountGroup g = newGroup("a");
+ Timestamp t1 = TimeUtil.nowTs();
+ Timestamp t2 = TimeUtil.nowTs();
+ Timestamp t3 = TimeUtil.nowTs();
+ GroupBundle b =
+ builder()
+ .group(g)
+ .byId(byId(g, "x"))
+ .byIdAudit(addById(g, "x", 8, t2), addAndRemoveById(g, "y", 8, t1, 9, t3))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(4);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(log.get(1), "Update group\n\nAdd-group: Group <y>", "Account 8", "8@server-id");
+ assertCommit(log.get(2), "Update group\n\nAdd-group: Group <x>", "Account 8", "8@server-id");
+ assertCommit(log.get(3), "Update group\n\nRemove-group: Group <y>", "Account 9", "9@server-id");
+ }
+
+ @Test
+ public void unauditedByIdAddedAtEnd() throws Exception {
+ AccountGroup g = newGroup("a");
+ GroupBundle b =
+ builder()
+ .group(g)
+ .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
+ .byIdAudit(addById(g, "x", 8, TimeUtil.nowTs()))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(3);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(log.get(1), "Update group\n\nAdd-group: Group <x>", "Account 8", "8@server-id");
+ assertServerCommit(log.get(2), "Update group\n\nAdd-group: Group <y>\nAdd-group: Group <z>");
+ }
+
+ @Test
+ public void auditsAtSameTimestampBrokenDownByType() throws Exception {
+ AccountGroup g = newGroup("a");
+ Timestamp ts = TimeUtil.nowTs();
+ int user = 8;
+ GroupBundle b =
+ builder()
+ .group(g)
+ .members(member(g, 1), member(g, 2))
+ .memberAudit(
+ addMember(g, 1, user, ts),
+ addMember(g, 2, user, ts),
+ addAndRemoveMember(g, 3, user, ts, user, ts))
+ .byId(byId(g, "x"), byId(g, "y"))
+ .byIdAudit(
+ addById(g, "x", user, ts),
+ addById(g, "y", user, ts),
+ addAndRemoveById(g, "z", user, ts, user, ts))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(5);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(
+ log.get(1),
+ "Update group\n"
+ + "\n"
+ + "Add: Account 1 <1@server-id>\n"
+ + "Add: Account 2 <2@server-id>\n"
+ + "Add: Account 3 <3@server-id>",
+ "Account 8",
+ "8@server-id");
+ assertCommit(
+ log.get(2), "Update group\n\nRemove: Account 3 <3@server-id>", "Account 8", "8@server-id");
+ assertCommit(
+ log.get(3),
+ "Update group\n"
+ + "\n"
+ + "Add-group: Group <x>\n"
+ + "Add-group: Group <y>\n"
+ + "Add-group: Group <z>",
+ "Account 8",
+ "8@server-id");
+ assertCommit(log.get(4), "Update group\n\nRemove-group: Group <z>", "Account 8", "8@server-id");
+ }
+
+ @Test
+ public void auditsAtSameTimestampBrokenDownByUserAndType() throws Exception {
+ AccountGroup g = newGroup("a");
+ Timestamp ts = TimeUtil.nowTs();
+ int user1 = 8;
+ int user2 = 9;
+
+ GroupBundle b =
+ builder()
+ .group(g)
+ .members(member(g, 1), member(g, 2), member(g, 3))
+ .memberAudit(
+ addMember(g, 1, user1, ts), addMember(g, 2, user2, ts), addMember(g, 3, user1, ts))
+ .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
+ .byIdAudit(
+ addById(g, "x", user1, ts), addById(g, "y", user2, ts), addById(g, "z", user1, ts))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(5);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(
+ log.get(1),
+ "Update group\n" + "\n" + "Add: Account 1 <1@server-id>\n" + "Add: Account 3 <3@server-id>",
+ "Account 8",
+ "8@server-id");
+ assertCommit(
+ log.get(2),
+ "Update group\n\nAdd-group: Group <x>\nAdd-group: Group <z>",
+ "Account 8",
+ "8@server-id");
+ assertCommit(
+ log.get(3), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 9", "9@server-id");
+ assertCommit(log.get(4), "Update group\n\nAdd-group: Group <y>", "Account 9", "9@server-id");
+ }
+
+ @Test
+ public void fixupCommitPostDatesAllAuditEventsEvenIfAuditEventsAreInTheFuture() throws Exception {
+ AccountGroup g = newGroup("a");
+ IntStream.range(0, 20).forEach(i -> TimeUtil.nowTs());
+ Timestamp future = TimeUtil.nowTs();
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+
+ GroupBundle b =
+ builder()
+ .group(g)
+ .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
+ .byIdAudit(addById(g, "x", 8, future))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(3);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(log.get(1), "Update group\n\nAdd-group: Group <x>", "Account 8", "8@server-id");
+ assertServerCommit(log.get(2), "Update group\n\nAdd-group: Group <y>\nAdd-group: Group <z>");
+
+ assertThat(log.stream().map(c -> c.committer.date).collect(toImmutableList()))
+ .named("%s", log)
+ .isOrdered();
+ assertThat(TimeUtil.nowTs()).isLessThan(future);
+ }
+
+ @Test
+ public void combineWithBatchGroupNameNotes() throws Exception {
+ AccountGroup g1 = newGroup("a");
+ AccountGroup g2 = newGroup("b");
+
+ GroupBundle b1 = builder().group(g1).build();
+ GroupBundle b2 = builder().group(g2).build();
+
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+
+ rebuilder.rebuild(repo, b1, bru);
+ rebuilder.rebuild(repo, b2, bru);
+ try (ObjectInserter inserter = repo.newObjectInserter()) {
+ ImmutableList<GroupReference> refs =
+ ImmutableList.of(GroupReference.forGroup(g1), GroupReference.forGroup(g2));
+ GroupNameNotes.updateGroupNames(repo, inserter, bru, refs, newPersonIdent());
+ inserter.flush();
+ }
+
+ assertThat(log(g1)).isEmpty();
+ assertThat(log(g2)).isEmpty();
+ assertThat(logGroupNames()).isEmpty();
+
+ RefUpdateUtil.executeChecked(bru, repo);
+
+ assertThat(log(g1)).hasSize(1);
+ assertThat(log(g2)).hasSize(1);
+ assertThat(logGroupNames()).hasSize(1);
+ assertThat(reload(g1)).isEqualTo(b1);
+ assertThat(reload(g2)).isEqualTo(b2);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+ }
+
+ private GroupBundle reload(AccountGroup g) throws Exception {
+ return bundleFactory.fromNoteDb(repo, g.getGroupUUID());
+ }
+
+ private AccountGroup newGroup(String name) {
+ int id = idCounter.incrementAndGet();
+ return new AccountGroup(
+ new AccountGroup.NameKey(name),
+ new AccountGroup.Id(id),
+ new AccountGroup.UUID(name + "-" + id),
+ TimeUtil.nowTs());
+ }
+
+ private AccountGroupMember member(AccountGroup g, int accountId) {
+ return new AccountGroupMember(new AccountGroupMember.Key(new Account.Id(accountId), g.getId()));
+ }
+
+ private AccountGroupMemberAudit addMember(
+ AccountGroup g, int accountId, int adder, Timestamp addedOn) {
+ return new AccountGroupMemberAudit(member(g, accountId), new Account.Id(adder), addedOn);
+ }
+
+ private AccountGroupMemberAudit addAndLegacyRemoveMember(
+ AccountGroup g, int accountId, int adder, Timestamp addedOn) {
+ AccountGroupMemberAudit a = addMember(g, accountId, adder, addedOn);
+ a.removedLegacy();
+ return a;
+ }
+
+ private AccountGroupMemberAudit addAndRemoveMember(
+ AccountGroup g,
+ int accountId,
+ int adder,
+ Timestamp addedOn,
+ int removedBy,
+ Timestamp removedOn) {
+ AccountGroupMemberAudit a = addMember(g, accountId, adder, addedOn);
+ a.removed(new Account.Id(removedBy), removedOn);
+ return a;
+ }
+
+ private AccountGroupByIdAud addById(
+ AccountGroup g, String subgroupUuid, int adder, Timestamp addedOn) {
+ return new AccountGroupByIdAud(byId(g, subgroupUuid), new Account.Id(adder), addedOn);
+ }
+
+ private AccountGroupByIdAud addAndRemoveById(
+ AccountGroup g,
+ String subgroupUuid,
+ int adder,
+ Timestamp addedOn,
+ int removedBy,
+ Timestamp removedOn) {
+ AccountGroupByIdAud a = addById(g, subgroupUuid, adder, addedOn);
+ a.removed(new Account.Id(removedBy), removedOn);
+ return a;
+ }
+
+ private AccountGroupById byId(AccountGroup g, String subgroupUuid) {
+ return new AccountGroupById(
+ new AccountGroupById.Key(g.getId(), new AccountGroup.UUID(subgroupUuid)));
+ }
+
+ private ImmutableList<CommitInfo> log(AccountGroup g) throws Exception {
+ return GroupTestUtil.log(repo, RefNames.refsGroups(g.getGroupUUID()));
+ }
+
+ private ImmutableList<CommitInfo> logGroupNames() throws Exception {
+ return GroupTestUtil.log(repo, REFS_GROUPNAMES);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index 74e1c09..d4ecb6d 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -18,6 +18,7 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Change;
@@ -29,10 +30,10 @@
@Ignore
public class FakeChangeIndex implements ChangeIndex {
- static Schema<ChangeData> V1 =
+ static final Schema<ChangeData> V1 =
new Schema<>(1, ImmutableList.<FieldDef<ChangeData, ?>>of(ChangeField.STATUS));
- static Schema<ChangeData> V2 =
+ static final Schema<ChangeData> V2 =
new Schema<>(2, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
private static class Source implements ChangeDataSource {
@@ -58,6 +59,11 @@
}
@Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ @Override
public String toString() {
return p.toString();
}
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index b9f6411..acb33e9 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -77,7 +77,7 @@
assertInvalidState("project1:refs/heads/foo:");
assertThat(
- StalenessChecker.parseStates(
+ RefState.parseStates(
byteArrays(
P1 + ":refs/heads/foo:" + SHA1,
P1 + ":refs/heads/bar:" + SHA2,
@@ -91,7 +91,7 @@
private static void assertInvalidState(String state) {
try {
- StalenessChecker.parseStates(byteArrays(state));
+ RefState.parseStates(byteArrays(state));
assert_().fail("expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java b/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
index 650262a..722dd08 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -16,7 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.common.TimeUtil.roundToSecond;
+import static com.google.gerrit.common.TimeUtil.truncateToSecond;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
@@ -1098,7 +1098,7 @@
PatchSet ps1 = new PatchSet(c.currentPatchSetId());
ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
ps1.setUploader(accountId);
- ps1.setCreatedOn(roundToSecond(TimeUtil.nowTs()));
+ ps1.setCreatedOn(truncateToSecond(TimeUtil.nowTs()));
PatchSet ps2 = clone(ps1);
ps2.setCreatedOn(TimeUtil.nowTs());
@@ -1152,7 +1152,7 @@
PatchSet ps1 = new PatchSet(c.currentPatchSetId());
ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
ps1.setUploader(accountId);
- ps1.setCreatedOn(roundToSecond(TimeUtil.nowTs()));
+ ps1.setCreatedOn(truncateToSecond(TimeUtil.nowTs()));
ps1.setPushCertificate("some cert");
PatchSet ps2 = clone(ps1);
ps2.setPushCertificate(ps2.getPushCertificate() + "\n\n");
@@ -1579,7 +1579,7 @@
new PatchSetApproval(
new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
(short) 1,
- roundToSecond(TimeUtil.nowTs()));
+ truncateToSecond(TimeUtil.nowTs()));
PatchSetApproval a2 = clone(a1);
a2.setGranted(TimeUtil.nowTs());
@@ -1827,7 +1827,7 @@
5,
accountId,
null,
- roundToSecond(TimeUtil.nowTs()));
+ truncateToSecond(TimeUtil.nowTs()));
PatchLineComment c2 = clone(c1);
c2.setWrittenOn(TimeUtil.nowTs());
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 6b4c5ea..61e62f7 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -19,13 +19,18 @@
import static org.junit.Assert.fail;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
@@ -41,11 +46,15 @@
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;
+import com.google.gerrit.server.account.externalids.ExternalIds;
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.index.account.AccountField;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
@@ -62,6 +71,7 @@
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -111,6 +121,10 @@
@Inject protected GitRepositoryManager repoManager;
+ @Inject protected AccountIndexCollection indexes;
+
+ @Inject protected ExternalIds externalIds;
+
protected LifecycleManager lifecycle;
protected Injector injector;
protected ReviewDb db;
@@ -424,6 +438,38 @@
assertQuery("name:" + quote(newName), user1);
}
+ @Test
+ public void rawDocument() throws Exception {
+ AccountInfo userInfo = gApi.accounts().id(user.getAccountId().get()).get();
+
+ Optional<FieldBundle> rawFields =
+ indexes
+ .getSearchIndex()
+ .getRaw(
+ new Account.Id(userInfo._accountId),
+ QueryOptions.create(
+ IndexConfig.createDefault(),
+ 0,
+ 1,
+ indexes.getSearchIndex().getSchema().getStoredFields().keySet()));
+
+ assertThat(rawFields.isPresent()).isTrue();
+ assertThat(rawFields.get().getValue(AccountField.ID)).isEqualTo(userInfo._accountId);
+
+ List<AccountExternalIdInfo> externalIdInfos = gApi.accounts().self().getExternalIds();
+ List<ByteArrayWrapper> blobs = new ArrayList<>();
+ for (AccountExternalIdInfo info : externalIdInfos) {
+ blobs.add(
+ new ByteArrayWrapper(externalIds.get(ExternalId.Key.parse(info.identity)).toByteArray()));
+ }
+ assertThat(rawFields.get().getValue(AccountField.EXTERNAL_ID_STATE)).hasSize(blobs.size());
+ assertThat(
+ Streams.stream(rawFields.get().getValue(AccountField.EXTERNAL_ID_STATE))
+ .map(b -> new ByteArrayWrapper(b))
+ .collect(toList()))
+ .containsExactlyElementsIn(blobs);
+ }
+
protected AccountInfo newAccount(String username) throws Exception {
return newAccountWithEmail(username, null);
}
@@ -590,4 +636,26 @@
protected static Iterable<Integer> ids(List<AccountInfo> accounts) {
return accounts.stream().map(a -> a._accountId).collect(toList());
}
+
+ /** Boiler plate code to check two byte arrays for equality */
+ private static class ByteArrayWrapper {
+ private byte[] arr;
+
+ private ByteArrayWrapper(byte[] arr) {
+ this.arr = arr;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof ByteArrayWrapper)) {
+ return false;
+ }
+ return Arrays.equals(arr, ((ByteArrayWrapper) other).arr);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(arr);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
new file mode 100644
index 0000000..e2a4d1f
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -0,0 +1,38 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+ABSTRACT_QUERY_TEST = ["AbstractQueryAccountsTest.java"]
+
+java_library(
+ name = "abstract_query_tests",
+ testonly = 1,
+ srcs = ABSTRACT_QUERY_TEST,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/index",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:truth",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
+
+junit_tests(
+ name = "lucene_query_test",
+ size = "large",
+ srcs = glob(
+ ["*.java"],
+ exclude = ABSTRACT_QUERY_TEST,
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
new file mode 100644
index 0000000..a1af6d0
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -0,0 +1,48 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+ABSTRACT_QUERY_TEST = ["AbstractQueryChangesTest.java"]
+
+java_library(
+ name = "abstract_query_tests",
+ testonly = 1,
+ srcs = ABSTRACT_QUERY_TEST,
+ visibility = ["//visibility:public"],
+ runtime_deps = ["//prolog:gerrit-prolog-common"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/index",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:gwtorm",
+ "//lib:truth",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/jgit/org.eclipse.jgit.junit:junit",
+ ],
+)
+
+junit_tests(
+ name = "lucene_query_test",
+ size = "large",
+ srcs = glob(
+ ["*.java"],
+ exclude = ABSTRACT_QUERY_TEST,
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:gwtorm",
+ "//lib:truth",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/jgit/org.eclipse.jgit.junit:junit",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index cc7b863..54c22f9 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -27,7 +27,10 @@
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -64,6 +67,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.junit.After;
import org.junit.Before;
@@ -384,6 +388,26 @@
assertQuery("description:" + newDescription, group1);
}
+ @Test
+ public void rawDocument() throws Exception {
+ GroupInfo group1 = createGroup(name("group1"));
+ AccountGroup.UUID uuid = new AccountGroup.UUID(group1.id);
+
+ Optional<FieldBundle> rawFields =
+ indexes
+ .getSearchIndex()
+ .getRaw(
+ uuid,
+ QueryOptions.create(
+ IndexConfig.createDefault(),
+ 0,
+ 10,
+ indexes.getSearchIndex().getSchema().getStoredFields().keySet()));
+
+ assertThat(rawFields.isPresent()).isTrue();
+ assertThat(rawFields.get().getValue(GroupField.UUID)).isEqualTo(uuid.get());
+ }
+
private Account.Id createAccountOutsideRequestContext(
String username, String fullName, String email, boolean active) throws Exception {
try (ManualRequestContext ctx = oneOffRequestContext.open()) {
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
new file mode 100644
index 0000000..49bc938
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -0,0 +1,38 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+ABSTRACT_QUERY_TEST = ["AbstractQueryGroupsTest.java"]
+
+java_library(
+ name = "abstract_query_tests",
+ testonly = 1,
+ srcs = ABSTRACT_QUERY_TEST,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/index",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:truth",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
+
+junit_tests(
+ name = "lucene_query_test",
+ size = "large",
+ srcs = glob(
+ ["*.java"],
+ exclude = ABSTRACT_QUERY_TEST,
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
new file mode 100644
index 0000000..d350faf
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -0,0 +1,37 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+ABSTRACT_QUERY_TEST = ["AbstractQueryProjectsTest.java"]
+
+java_library(
+ name = "abstract_query_tests",
+ testonly = 1,
+ srcs = ABSTRACT_QUERY_TEST,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:truth",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
+
+junit_tests(
+ name = "lucene_query_test",
+ size = "large",
+ srcs = glob(
+ ["*.java"],
+ exclude = ABSTRACT_QUERY_TEST,
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/rules/BUILD b/javatests/com/google/gerrit/server/rules/BUILD
new file mode 100644
index 0000000..04a6485
--- /dev/null
+++ b/javatests/com/google/gerrit/server/rules/BUILD
@@ -0,0 +1,19 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "prolog_tests",
+ srcs = glob(["*.java"]),
+ resource_strip_prefix = "prologtests",
+ resources = ["//prologtests:gerrit_common_test"],
+ deps = [
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/project/testing:project-test-util",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:truth",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/prolog:runtime",
+ "//prolog:gerrit-prolog-common",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/rules/PrologTestCase.java b/javatests/com/google/gerrit/server/rules/PrologTestCase.java
index 5fc6ce7..e8eea2d 100644
--- a/javatests/com/google/gerrit/server/rules/PrologTestCase.java
+++ b/javatests/com/google/gerrit/server/rules/PrologTestCase.java
@@ -41,8 +41,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import org.junit.Ignore;
/** Base class for any tests written in Prolog. */
+@Ignore
public abstract class PrologTestCase extends GerritBaseTests {
private static final SymbolTerm test_1 = SymbolTerm.intern("test", 1);
diff --git a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index bdcee40..e4b0da5 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.testing.InMemoryDatabase;
import com.google.gerrit.testing.InMemoryH2Type;
@@ -115,6 +116,7 @@
bind(SystemGroupBackend.class);
install(new NotesMigration.Module());
+ install(new GroupsMigration.Module());
bind(MetricMaker.class).to(DisabledMetricMaker.class);
}
})
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 51d3e47..9745f9e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -212,11 +212,11 @@
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
*
- * @return {Promise<boolean>} A promise that yields true if the latest patch
+ * @return {Promise<!Object>} A promise that yields true if the latest patch
* has been loaded, and false if a newer patch has been uploaded in the
* meantime. The promise is rejected on network error.
*/
- fetchIsLatestKnown(change, restAPI) {
+ fetchChangeUpdates(change, restAPI) {
const knownLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
Gerrit.PatchSetBehavior.computeAllPatchSets(change));
return restAPI.getChangeDetail(change._number)
@@ -226,7 +226,11 @@
}
const actualLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
Gerrit.PatchSetBehavior.computeAllPatchSets(detail));
- return actualLatest <= knownLatest;
+ return {
+ isLatest: actualLatest <= knownLatest,
+ newStatus: change.status !== detail.status ? detail.status : null,
+ newMessages: change.messages.length < detail.messages.length,
+ };
});
},
@@ -241,6 +245,16 @@
const findNum = rev => rev._number + '' === patchNum + '';
return revisions.findIndex(findNum);
},
+
+ /**
+ * Convert parent indexes from patch range expressions to numbers.
+ * For example, in a patch range expression `"-3"` becomes `3`.
+ * @param {number|string} rangeBase
+ * @return {number}
+ */
+ getParentIndex(rangeBase) {
+ return -parseInt(rangeBase + '', 10);
+ },
};
})(window);
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index afdb4ac..7116c5d 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -35,31 +35,37 @@
assert.equal(get(revisions, '3'), undefined);
});
- test('fetchIsLatestKnown on latest', done => {
+ test('fetchChangeUpdates on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
+ status: 'NEW',
+ messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(knownChange);
},
};
- Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
- .then(isLatest => {
- assert.isTrue(isLatest);
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
done();
});
});
- test('fetchIsLatestKnown not on latest', done => {
+ test('fetchChangeUpdates not on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
+ status: 'NEW',
+ messages: [],
};
const actualChange = {
revisions: {
@@ -67,15 +73,81 @@
sha2: {description: 'patch 2', _number: 2},
sha3: {description: 'patch 3', _number: 3},
},
+ status: 'NEW',
+ messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
- Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
- .then(isLatest => {
- assert.isFalse(isLatest);
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isFalse(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new status', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'MERGED',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.equal(result.newStatus, 'MERGED');
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new messages', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [{message: 'blah blah'}],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isTrue(result.newMessages);
done();
});
});
@@ -245,5 +317,10 @@
sorted.splice(2, 0, edit);
assert.deepEqual(sort(revisions), sorted);
});
+
+ test('getParentIndex', () => {
+ assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
+ assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
+ });
});
</script>
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 9fe10bc..2d84179 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
@@ -70,10 +70,10 @@
confirm-label="Create"
on-confirm="_handleCreateGroup"
on-cancel="_handleCloseCreate">
- <div class="header">
+ <div class="header" slot="header">
Create Group
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-group-dialog
has-new-group-name="{{_hasNewGroupName}}"
params="[[params]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
index e132100..da20181 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
@@ -30,8 +30,8 @@
confirm-label="Delete [[_computeItemName(itemType)]]"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">[[_computeItemName(itemType)]] Deletion</div>
- <div class="main">
+ <div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
+ <div class="main" slot="main">
<label for="branchInput">
Do you really want to delete the following [[_computeItemName(itemType)]]?
</label>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
index f43403a..4274749 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
@@ -20,6 +20,7 @@
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -76,10 +77,10 @@
disabled="[[!_canCreate]]"
on-confirm="_handleCreateChange"
on-cancel="_handleCloseCreateChange">
- <div class="header">
+ <div class="header" slot="header">
Create Change
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-change-dialog
id="createNewChangeModal"
can-create="{{_canCreate}}"
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
index 85e8f83..0ed278a 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
@@ -187,10 +187,10 @@
confirm-label="Create"
on-confirm="_handleCreateItem"
on-cancel="_handleCloseCreate">
- <div class="header">
+ <div class="header" slot="header">
Create [[_computeItemName(detailType)]]
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-pointer-dialog
id="createNewModal"
detail-type="[[_computeItemName(detailType)]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
index 6c45704..958d2dc 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
@@ -81,10 +81,10 @@
confirm-label="Create"
on-confirm="_handleCreateProject"
on-cancel="_handleCloseCreate">
- <div class="header">
+ <div class="header" slot="header">
Create Project
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-project-dialog
has-new-project-name="{{_hasNewProjectName}}"
params="[[params]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project.js b/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
index 54b02aa..06d553a 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
@@ -165,7 +165,7 @@
_formatBooleanSelect(item) {
if (!item) { return; }
let inheritLabel = 'Inherit';
- if (item.inherited_value) {
+ if (!(item.inherited_value === undefined)) {
inheritLabel = `Inherit (${item.inherited_value})`;
}
return [
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html b/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
index a86bc03..4f9b098 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
@@ -174,7 +174,7 @@
});
test('_formatBooleanSelect', () => {
- let item = {inherited_value: 'true'};
+ let item = {inherited_value: true};
assert.deepEqual(element._formatBooleanSelect(item), [
{
label: 'Inherit (true)',
@@ -189,6 +189,21 @@
},
]);
+ item = {inherited_value: false};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit (false)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
// For items without inherited values
item = {};
assert.deepEqual(element._formatBooleanSelect(item), [
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 79a9245..481cd76 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -52,11 +52,6 @@
/* px because don't have the same font size */
margin-left: 12px;
}
- gr-button[data-action-key='submit'] {
- --gr-button-background: #f67070;
- --gr-button-color: #fff;
- --gr-button-hover-background-color: #dc5151;
- }
#actionLoadingMessage {
align-items: center;
color: #777;
@@ -107,11 +102,6 @@
on-tap="_handleActionTap">[[action.label]]</gr-button>
</template>
</section>
- <gr-button
- class="reply"
- secondary
- disabled="[[replyDisabled]]"
- on-tap="_handleReplyTap">[[replyButtonLabel]]</gr-button>
<section id="secondaryActions"
hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
<template
@@ -179,10 +169,10 @@
confirm-label="Delete"
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleDeleteConfirm">
- <div class="header">
+ <div class="header" slot="header">
Delete Change
</div>
- <div class="main">
+ <div class="main" slot="main">
Do you really want to delete the change?
</div>
</gr-confirm-dialog>
@@ -192,10 +182,10 @@
confirm-label="Delete"
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleDeleteEditConfirm">
- <div class="header">
+ <div class="header" slot="header">
Delete Change Edit
</div>
- <div class="main">
+ <div class="main" slot="main">
Do you really want to delete the edit?
</div>
</gr-confirm-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index c86c251..b2a6f0d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -160,12 +160,6 @@
* @event reload-change
*/
- /**
- * Fired when the reply button is tapped.
- *
- * @event reply-tap
- */
-
/**
* Fired when an action is tapped.
*
@@ -214,8 +208,6 @@
type: Object,
value() { return {}; },
},
- replyButtonLabel: String,
- replyDisabled: Boolean,
_loading: {
type: Boolean,
@@ -729,11 +721,6 @@
this._showActionDialog(this.$.confirmRevertDialog);
},
- _handleReplyTap(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('reply-tap'));
- },
-
_handleActionTap(e) {
e.preventDefault();
const el = Polymer.dom(e).localTarget;
@@ -1062,9 +1049,9 @@
this._handleResponseError(response);
};
- return this.fetchIsLatestKnown(this.change, this.$.restAPI)
- .then(isLatest => {
- if (!isLatest) {
+ return this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ if (!result.isLatest) {
this.fire('show-alert', {
message: 'Cannot set label: a newer patch has been ' +
'uploaded to this change.',
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index a3932c2..62b4626 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -194,7 +194,7 @@
const buttonEls = Polymer.dom(element.root)
.querySelectorAll('gr-button');
const menuItems = element.$.moreActions.items;
- assert.equal(buttonEls.length + menuItems.length, 7);
+ assert.equal(buttonEls.length + menuItems.length, 6);
assert.isFalse(element.hidden);
done();
});
@@ -241,8 +241,8 @@
test('submit change', done => {
sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
.returns(Promise.resolve('test'));
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: true}); });
element.change = {
revisions: {
rev1: {_number: 1},
@@ -1264,8 +1264,8 @@
let sendStub;
setup(() => {
- sandbox.stub(element, 'fetchIsLatestKnown')
- .returns(Promise.resolve(true));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
sendStub = sandbox.stub(element.$.restAPI, 'getChangeURLAndSend')
.returns(Promise.resolve({}));
});
@@ -1293,8 +1293,8 @@
suite('failure modes', () => {
test('non-latest', () => {
- sandbox.stub(element, 'fetchIsLatestKnown')
- .returns(Promise.resolve(false));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: false}));
const sendStub = sandbox.stub(element.$.restAPI,
'getChangeURLAndSend');
@@ -1307,8 +1307,8 @@
});
test('send fails', () => {
- sandbox.stub(element, 'fetchIsLatestKnown')
- .returns(Promise.resolve(true));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
const sendStub = sandbox.stub(element.$.restAPI,
'getChangeURLAndSend',
(num, method, patchNum, endpoint, payload, onErr) => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index a999641..caf2fa3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,11 +14,13 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
+<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
@@ -327,6 +329,10 @@
</template>
</span>
</section>
+ <gr-endpoint-decorator name="change-metadata-item">
+ <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[currentRevision]]"></gr-endpoint-param>
+ </gr-endpoint-decorator>
</gr-external-style>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index db6c44c..00b4c2a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -37,6 +37,8 @@
properties: {
/** @type {?} */
change: Object,
+ /** @type {?} */
+ currentRevision: Object,
commitInfo: Object,
mutable: Boolean,
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 522d34e..0b88f22 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -38,6 +38,9 @@
setup(() => {
sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
getLoggedIn() { return Promise.resolve(false); },
@@ -615,5 +618,29 @@
});
});
});
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element.change = {labels: {}};
+ element.currentRevision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-metadata-item').getLastAttached().then(
+ el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ Gerrit._setPluginsCount(0);
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element.change);
+ assert.strictEqual(hookEl.revision, element.currentRevision);
+ done();
+ });
+ });
+ });
});
</script>
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 fa53bfe..56548de 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
@@ -15,7 +15,6 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
-
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
@@ -25,6 +24,7 @@
<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../../edit/gr-edit-constants.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
@@ -87,6 +87,9 @@
.header-title .headerSubject {
font-family: var(--font-family-bold);
}
+ .replyContainer {
+ margin-bottom: 1em;
+ }
gr-change-star {
margin-right: .25em;
vertical-align: -.425em;
@@ -133,6 +136,10 @@
}
.editCommitMessage {
margin-top: 1em;
+ --gr-button: {
+ padding-left: 0;
+ padding-right: 0;
+ }
}
.changeStatuses,
.commitActions {
@@ -343,14 +350,14 @@
edit-loaded="[[_editLoaded]]"
edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
on-reload-change="_handleReloadChange"
- on-download-tap="_handleOpenDownloadDialog"
- on-reply-tap="_handleReplyTap"></gr-change-actions>
+ on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
</div><!-- end commit actions -->
</div><!-- end header -->
<section class="changeInfo">
<div class="changeInfo-column changeMetadata hideOnMobileOverlay">
<gr-change-metadata
change="{{_change}}"
+ revision="[[_currentRevision]]"
commit-info="[[_commitInfo]]"
server-config="[[_serverConfig]]"
mutable="[[_loggedIn]]"
@@ -365,6 +372,14 @@
<hr class="mobile">
<div id="commitAndRelated" class="hideOnMobileOverlay">
<div class="commitContainer">
+ <div class="replyContainer">
+ <gr-button
+ id="replyBtn"
+ class="reply"
+ secondary
+ disabled="[[_replyDisabled]]"
+ on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+ </div>
<div
id="commitMessage"
class$="commitMessage [[_computeCommitClass(_commitCollapsed, _latestCommitMessage)]]">
@@ -435,7 +450,7 @@
all-patch-sets="[[_allPatchSets]]"
change="[[_change]]"
change-num="[[_changeNum]]"
- comments="[[_changeComments.comments]]"
+ change-comments="[[_changeComments]]"
commit-info="[[_commitInfo]]"
change-url="[[_computeChangeUrl(_change)]]"
edit-loaded="[[_editLoaded]]"
@@ -472,6 +487,10 @@
on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
</section>
<gr-endpoint-decorator name="change-view-integration">
+ <gr-endpoint-param name="change" value="[[_change]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[_currentRevision]]">
+ </gr-endpoint-param>
</gr-endpoint-decorator>
<gr-messages-list id="messageList"
class="hideOnMobileOverlay"
@@ -495,7 +514,6 @@
class="scrollable"
no-cancel-on-outside-click
no-cancel-on-esc-key
- on-iron-overlay-opened="_handleReplyOverlayOpen"
with-backdrop>
<gr-reply-dialog id="replyDialog"
change="{{_change}}"
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 f00f386..3d7d79a 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
@@ -41,6 +41,14 @@
const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
+ const ReloadToastMessage = {
+ NEWER_REVISION: 'A newer patch set has been uploaded',
+ RESTORED: 'This change has been restored',
+ ABANDONED: 'This change has been abandoned',
+ MERGED: 'This change has been merged',
+ NEW_MESSAGE: 'There are new messages on this change',
+ };
+
Polymer({
is: 'gr-change-view',
@@ -158,6 +166,7 @@
type: Boolean,
value: true,
},
+ _currentRevision: Object,
_currentRevisionActions: Object,
_allPatchSets: {
type: Array,
@@ -410,7 +419,7 @@
_handleReplyTap(e) {
e.preventDefault();
- this._openReplyDialog();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
},
_handleOpenDiffPrefs() {
@@ -438,15 +447,7 @@
this.$.replyDialog.draft = quoteStr;
}
this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog();
- },
-
- _handleReplyOverlayOpen(e) {
- // This is needed so that focus is not set on the reply overlay
- // when the suggestion overaly from gr-autogrow-textarea opens.
- if (e.target === this.$.replyOverlay) {
- this.$.replyDialog.focus();
- }
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
},
_handleHideBackgroundContent() {
@@ -628,7 +629,7 @@
if (!loggedIn) { return; }
if (this.viewState.showReplyDialog) {
- this._openReplyDialog();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
// TODO(kaspern@): Find a better signal for when to call center.
this.async(() => { this.$.replyOverlay.center(); }, 100);
this.async(() => { this.$.replyOverlay.center(); }, 1000);
@@ -775,7 +776,7 @@
}
e.preventDefault();
- this._openReplyDialog();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
});
},
@@ -983,6 +984,7 @@
parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
this._change = change;
+ this._currentRevision = currentRevision;
if (!this._patchRange || !this._patchRange.patchNum ||
this.patchNumEquals(this._patchRange.patchNum,
currentRevision._number)) {
@@ -1249,24 +1251,37 @@
}
this._updateCheckTimerHandle = this.async(() => {
- this.fetchIsLatestKnown(this._change, this.$.restAPI)
- .then(latest => {
- if (latest) {
- this._startUpdateCheckTimer();
- } else {
- this._cancelUpdateCheckTimer();
- this.fire('show-alert', {
- message: 'A newer patch set has been uploaded.',
- // Persist this alert.
- dismissOnNavigation: true,
- action: 'Reload',
- callback: function() {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this._change);
- }.bind(this),
- });
- }
- });
+ this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
+ let toastMessage = null;
+ if (!result.isLatest) {
+ toastMessage = ReloadToastMessage.NEWER_REVISION;
+ } else if (result.newStatus === this.ChangeStatus.MERGED) {
+ toastMessage = ReloadToastMessage.MERGED;
+ } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+ toastMessage = ReloadToastMessage.ABANDONED;
+ } else if (result.newStatus === this.ChangeStatus.NEW) {
+ toastMessage = ReloadToastMessage.RESTORED;
+ } else if (result.newMessages) {
+ toastMessage = ReloadToastMessage.NEW_MESSAGE;
+ }
+
+ if (!toastMessage) {
+ this._startUpdateCheckTimer();
+ return;
+ }
+
+ this._cancelUpdateCheckTimer();
+ this.fire('show-alert', {
+ message: toastMessage,
+ // Persist this alert.
+ dismissOnNavigation: true,
+ action: 'Reload',
+ callback: function() {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this._change);
+ }.bind(this),
+ });
+ });
}, this._serverConfig.change.update_delay * 1000);
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index aeb98d5..d3e1d72 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -49,6 +49,11 @@
setup(() => {
sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ // Since _endpoints are global, must reset state.
+ Gerrit._endpoints = new GrPluginEndpoints();
navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({test: 'config'}); },
@@ -57,6 +62,7 @@
});
element = fixture('basic');
sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
+ Gerrit._setPluginsCount(0);
});
teardown(done => {
@@ -120,14 +126,20 @@
test('A toggles overlay when logged in', done => {
sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown')
- .returns(Promise.resolve(true));
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
element._change = {labels: {}};
+ const openSpy = sandbox.spy(element, '_openReplyDialog');
+
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isTrue(element.$.replyOverlay.opened);
element.$.replyOverlay.close();
assert.isFalse(element.$.replyOverlay.opened);
+ assert(openSpy.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openSpy.callCount, 1);
done();
});
});
@@ -834,6 +846,27 @@
});
});
+ test('_openReplyDialog called with `ANY` when coming from tap event',
+ () => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element._serverConfig = {};
+ MockInteractions.tap(element.$.replyBtn);
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openStub.callCount, 1);
+ });
+
+ test('_openReplyDialog called with `BODY` when coming from message reply' +
+ 'event', () => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element.$.messageList.fire('reply', {message: {message: 'text'}});
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.BODY),
+ '_openReplyDialog should have been passed BODY');
+ assert.equal(openStub.callCount, 1);
+ });
+
test('reply dialog focus can be controlled', () => {
const FocusTarget = element.$.replyDialog.FocusTarget;
const openStub = sandbox.stub(element, '_openReplyDialog');
@@ -842,11 +875,13 @@
element._handleShowReplyDialog(e);
assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
'_openReplyDialog should have been passed REVIEWERS');
+ assert.equal(openStub.callCount, 1);
e.detail.value = {ccsOnly: true};
element._handleShowReplyDialog(e);
assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
'_openReplyDialog should have been passed CCS');
+ assert.equal(openStub.callCount, 2);
});
test('getUrlParameter functionality', () => {
@@ -940,8 +975,8 @@
suite('reply dialog tests', () => {
setup(() => {
sandbox.stub(element.$.replyDialog, '_draftChanged');
- sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: true}); });
element._change = {labels: {}};
});
@@ -991,8 +1026,8 @@
suite('commit message expand/collapse', () => {
setup(() => {
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(false); });
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: false}); });
});
test('commitCollapseToggle hidden for short commit message', () => {
@@ -1142,29 +1177,58 @@
});
test('_startUpdateCheckTimer negative delay', () => {
- sandbox.stub(element, 'fetchIsLatestKnown');
+ sandbox.stub(element, 'fetchChangeUpdates');
element._serverConfig = {change: {update_delay: -1}};
assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isFalse(element.fetchIsLatestKnown.called);
+ assert.isFalse(element.fetchChangeUpdates.called);
});
test('_startUpdateCheckTimer up-to-date', () => {
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: true}); });
element._serverConfig = {change: {update_delay: 12345}};
assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isTrue(element.fetchIsLatestKnown.called);
+ assert.isTrue(element.fetchChangeUpdates.called);
assert.equal(element.async.lastCall.args[1], 12345 * 1000);
});
test('_startUpdateCheckTimer out-of-date shows an alert', done => {
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(false); });
- element.addEventListener('show-alert', () => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: false}); });
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'A newer patch set has been uploaded');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new status shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newStatus: element.ChangeStatus.MERGED,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message, 'This change has been merged');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new messages shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newMessages: true,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'There are new messages on this change');
done();
});
element._serverConfig = {change: {update_delay: 12345}};
@@ -1315,5 +1379,28 @@
assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo');
assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
});
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element._change = {labels: {}};
+ element._currentRevision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-view-integration').getLastAttached().then(
+ el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element._change);
+ assert.strictEqual(hookEl.revision, element._currentRevision);
+ done();
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
index d3bf159..a18e2a2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -55,8 +55,8 @@
confirm-label="Abandon"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Abandon Change</div>
- <div class="main">
+ <div class="header" slot="header">Abandon Change</div>
+ <div class="main" slot="main">
<label for="messageInput">Abandon Message</label>
<iron-autogrow-textarea
id="messageInput"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
index 5151280..34688ad 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -62,8 +62,8 @@
confirm-label="Cherry Pick"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Cherry Pick Change to Another Branch</div>
- <div class="main">
+ <div class="header" slot="header">Cherry Pick Change to Another Branch</div>
+ <div class="main" slot="main">
<label for="branchInput">
Cherry Pick to branch
</label>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
index ec6bfb4..2e530d9 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
@@ -61,8 +61,8 @@
confirm-label="Move Change"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Move Change to Another Branch</div>
- <div class="main">
+ <div class="header" slot="header">Move Change to Another Branch</div>
+ <div class="main" slot="main">
<p class="warning">
Warning: moving a change will not change its parents.
</p>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 2772594..582a03c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -52,8 +52,8 @@
confirm-label="Rebase"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Confirm rebase</div>
- <div class="main">
+ <div class="header" slot="header">Confirm rebase</div>
+ <div class="main" slot="main">
<div id="rebaseOnParent" class="rebaseOption"
hidden$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]">
<input id="rebaseOnParentInput"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 92e8de3..5b39547 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -50,8 +50,8 @@
confirm-label="Revert"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Revert Merged Change</div>
- <div class="main">
+ <div class="header" slot="header">Revert Merged Change</div>
+ <div class="main" slot="main">
<label for="messageInput">
Revert Commit Message
</label>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index c8f724d..f46af09 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -21,6 +21,7 @@
<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
<link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
+<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -100,6 +101,9 @@
gr-button.selected iron-icon {
color: var(--color-link);
}
+ gr-linked-chip {
+ --linked-chip-text-color: black;
+ }
.expanded #collapseBtn,
.openFile .fileViewActions {
align-items: center;
@@ -145,7 +149,7 @@
<h3 class="label">Files</h3>
<gr-patch-range-select
id="rangeSelect"
- comments="[[comments]]"
+ change-comments="[[changeComments]]"
change-num="[[changeNum]]"
patch-num="[[patchNum]]"
base-patch-num="[[basePatchNum]]"
@@ -164,14 +168,28 @@
</span>
<span class="container descriptionContainer hideOnEdit">
<span class="separator"></span>
- <gr-editable-label
- id="descriptionLabel"
- class="descriptionLabel"
- label-text="Add patchset description"
- value="[[_computePatchSetDescription(change, patchNum)]]"
- placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
- read-only="[[_descriptionReadOnly]]"
- on-changed="_handleDescriptionChanged"></gr-editable-label>
+ <template
+ is="dom-if"
+ if="[[_patchsetDescription]]">
+ <gr-linked-chip
+ id="descriptionChip"
+ text="[[_patchsetDescription]]"
+ removable="[[!_descriptionReadOnly]]"
+ on-remove="_handleDescriptionRemoved"></gr-linked-chip>
+ </template>
+ <template
+ is="dom-if"
+ if="[[!_patchsetDescription]]">
+ <gr-editable-label
+ id="descriptionLabel"
+ uppercase
+ class="descriptionLabel"
+ label-text="Add patchset description"
+ value="[[_patchsetDescription]]"
+ placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
+ read-only="[[_descriptionReadOnly]]"
+ on-changed="_handleDescriptionChanged"></gr-editable-label>
+ </template>
</span>
</div>
<div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 07db340..123b156 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -27,7 +27,7 @@
change: Object,
changeNum: String,
changeUrl: String,
- comments: Object,
+ changeComments: Object,
commitInfo: Object,
editLoaded: Boolean,
loggedIn: Boolean,
@@ -48,6 +48,10 @@
readOnly: true,
value: 225,
},
+ _patchsetDescription: {
+ type: String,
+ value: '',
+ },
_descriptionReadOnly: {
type: Boolean,
computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
@@ -67,6 +71,10 @@
Gerrit.PatchSetBehavior,
],
+ observers: [
+ '_computePatchSetDescription(change, patchNum)',
+ ],
+
_expandAllDiffs() {
this._expanded = true;
this.fire('expand-diffs');
@@ -111,10 +119,14 @@
_computePatchSetDescription(change, patchNum) {
const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
- return (rev && rev.description) ?
+ this._patchsetDescription = (rev && rev.description) ?
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
+ _handleDescriptionRemoved(e) {
+ return this._updateDescription('', e);
+ },
+
/**
* @param {!Object} revisions The revisions object keyed by revision hashes
* @param {?Object} patchSet A revision already fetched from {revisions}
@@ -131,15 +143,31 @@
_handleDescriptionChanged(e) {
const desc = e.detail.trim();
+ this._updateDescription(desc, e);
+ },
+
+ /**
+ * Update the patchset description with the rest API.
+ * @param {string} desc
+ * @param {?(Event|Node)} e
+ * @return {!Promise}
+ */
+ _updateDescription(desc, e) {
+ const target = Polymer.dom(e).rootTarget;
+ if (target) { target.disabled = true; }
const rev = this.getRevisionByPatchNum(this.change.revisions,
this.patchNum);
const sha = this._getPatchsetHash(this.change.revisions, rev);
- this.$.restAPI.setDescription(this.changeNum,
- this.patchNum, desc)
+ return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
.then(res => {
if (res.ok) {
+ if (target) { target.disabled = false; }
this.set(['_change', 'revisions', sha, 'description'], desc);
+ this._patchsetDescription = desc;
}
+ }).catch(err => {
+ if (target) { target.disabled = false; }
+ return;
});
},
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index d47023f..8dbb06f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -96,10 +96,9 @@
'Add patchset description');
});
- test('_handleDescriptionChanged', () => {
+ test('description editing', () => {
const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
.returns(Promise.resolve({ok: true}));
- sandbox.stub(element, '_computeDescriptionReadOnly');
element.changeNum = '42';
element.basePatchNum = 'PARENT';
@@ -120,14 +119,44 @@
element.loggedIn = true;
flushAsynchronousOperations();
- const label = element.$.descriptionLabel;
- assert.equal(label.value, 'test');
- label.editing = true;
- label._inputText = 'test2';
- label._save();
- flushAsynchronousOperations();
- assert.isTrue(putDescStub.called);
- assert.equal(putDescStub.args[0][2], 'test2');
+
+ // The element has a description, so the account chip should be visible
+ // and the description label should not exist.
+ const chip = Polymer.dom(element.root).querySelector('#descriptionChip');
+ let label = Polymer.dom(element.root).querySelector('#descriptionLabel');
+
+ assert.equal(chip.text, 'test');
+ assert.isNotOk(label);
+
+ // Simulate tapping the remove button, but call function directly so that
+ // can determine what happens after the promise is resolved.
+ return element._handleDescriptionRemoved().then(() => {
+ // The API stub should be called with an empty string for the new
+ // description.
+ assert.equal(putDescStub.lastCall.args[2], '');
+
+ flushAsynchronousOperations();
+ // The editable label should now be visible and the chip hidden.
+ label = Polymer.dom(element.root).querySelector('#descriptionLabel');
+ assert.isOk(label);
+ assert.equal(getComputedStyle(chip).display, 'none');
+ assert.notEqual(getComputedStyle(label).display, 'none');
+ assert.isFalse(label.readOnly);
+ // Edit the label to have a new value of test2, and save.
+ label.editing = true;
+ label._inputText = 'test2';
+ label._save();
+ flushAsynchronousOperations();
+ // The API stub should be called with an `test2` for the new
+ // description.
+ assert.equal(putDescStub.callCount, 2);
+ assert.equal(putDescStub.lastCall.args[2], 'test2');
+ }).then(() => {
+ flushAsynchronousOperations();
+ // The chip should be visible again, and the label hidden.
+ assert.equal(getComputedStyle(label).display, 'none');
+ assert.notEqual(getComputedStyle(chip).display, 'none');
+ });
});
test('expandAllDiffs called when expand button clicked', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index c2b3f87..ae12592 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -29,6 +29,7 @@
<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
+<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
<link rel="import" href="../gr-file-list-constants.html">
@@ -266,17 +267,16 @@
</span>
<div class="comments desktop">
<span class="drafts">
- [[_computeDraftsString(changeComments.drafts, patchRange.patchNum, file.__path)]]
+ [[_computeDraftsString(changeComments, patchRange.patchNum, file.__path)]]
</span>
- [[_computeCommentsString(changeComments.comments, patchRange.patchNum, file.__path)]]
- [[_computeUnresolvedString(changeComments.comments, changeComments.drafts, patchRange.patchNum, file.__path)]]
+ [[_computeCommentsString(changeComments, patchRange.patchNum, file.__path)]]
</div>
<div class="comments mobile">
<span class="drafts">
- [[_computeDraftsStringMobile(changeComments.drafts, patchRange.patchNum,
+ [[_computeDraftsStringMobile(changeComments, patchRange.patchNum,
file.__path)]]
</span>
- [[_computeCommentsStringMobile(changeComments.comments, patchRange.patchNum,
+ [[_computeCommentsStringMobile(changeComments, patchRange.patchNum,
file.__path)]]
</div>
<div class$="[[_computeClass('stats', file.__path)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index cc1ee7d..4f7f89e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -309,89 +309,67 @@
this.$.diffCursor.handleDiffUpdate();
},
- _computeCommentsString(comments, patchNum, path) {
- return this._computeCountString(comments, patchNum, path, 'comment');
- },
-
- _computeDraftsString(drafts, patchNum, path) {
- return this._computeCountString(drafts, patchNum, path, 'draft');
- },
-
- _computeDraftsStringMobile(drafts, patchNum, path) {
- const draftCount = this._computeCountString(drafts, patchNum, path);
- return draftCount ? draftCount + 'd' : '';
- },
-
- _computeCommentsStringMobile(comments, patchNum, path) {
- const commentCount = this._computeCountString(comments, patchNum, path);
- return commentCount ? commentCount + 'c' : '';
- },
-
- getCommentsForPath(comments, patchNum, path) {
- return (comments[path] || []).filter(c => {
- return this.patchNumEquals(c.patch_set, patchNum);
- });
- },
-
/**
- * @param {!Array} comments
- * @param {number} patchNum
- * @param {string} path
- * @param {string=} opt_noun
- */
- _computeCountString(comments, patchNum, path, opt_noun) {
- if (!comments) { return ''; }
-
- const patchComments = this.getCommentsForPath(comments, patchNum, path);
- const num = patchComments.length;
- if (num === 0) { return ''; }
- if (!opt_noun) { return num; }
- const output = num + ' ' + opt_noun + (num > 1 ? 's' : '');
- return output;
- },
-
- /**
- * Computes a string counting the number of unresolved comment threads in a
- * given file and path.
+ * Computes a string with the number of comments and unresolved comments.
*
- * @param {!Object} comments
- * @param {!Object} drafts
+ * @param {!Object} changeComments
* @param {number} patchNum
* @param {string} path
* @return {string}
*/
- _computeUnresolvedString(comments, drafts, patchNum, path) {
- const unresolvedNum = this.computeUnresolvedNum(
- comments, drafts, patchNum, path);
- return unresolvedNum === 0 ? '' : '(' + unresolvedNum + ' unresolved)';
+ _computeCommentsString(changeComments, patchNum, path) {
+ const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
+ path);
+ const commentCount = changeComments.computeCommentCount(patchNum, path);
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ return commentString +
+ // Add a space if both comments and unresolved
+ (commentString && unresolvedString ? ' ' : '') +
+ // Add parentheses around unresolved if it exists.
+ (unresolvedString ? `(${unresolvedString})` : '');
},
- computeUnresolvedNum(comments, drafts, patchNum, path) {
- comments = this.getCommentsForPath(comments, patchNum, path);
- drafts = this.getCommentsForPath(drafts, patchNum, path);
- comments = comments.concat(drafts);
+ /**
+ * Computes a string with the number of drafts.
+ *
+ * @param {!Object} changeComments
+ * @param {number} patchNum
+ * @param {string} path
+ * @return {string}
+ */
+ _computeDraftsString(changeComments, patchNum, path) {
+ const draftCount = changeComments.computeDraftCount(patchNum, path);
+ return GrCountStringFormatter.computePluralString(draftCount, 'draft');
+ },
- // Create an object where every comment ID is the key of an unresolved
- // comment.
+ /**
+ * Computes a shortened string with the number of drafts.
+ *
+ * @param {!Object} changeComments
+ * @param {number} patchNum
+ * @param {string} path
+ * @return {string}
+ */
+ _computeDraftsStringMobile(changeComments, patchNum, path) {
+ const draftCount = changeComments.computeDraftCount(patchNum, path);
+ return GrCountStringFormatter.computeShortString(draftCount, 'd');
+ },
- const idMap = comments.reduce((acc, comment) => {
- if (comment.unresolved) {
- acc[comment.id] = true;
- }
- return acc;
- }, {});
-
- // Set false for the comments that are marked as parents.
- for (const comment of comments) {
- idMap[comment.in_reply_to] = false;
- }
-
- // The unresolved comments are the comments that still have true.
- const unresolvedLeaves = Object.keys(idMap).filter(key => {
- return idMap[key];
- });
-
- return unresolvedLeaves.length;
+ /**
+ * Computes a shortened string with the number of comments.
+ *
+ * @param {!Object} changeComments
+ * @param {number} patchNum
+ * @param {string} path
+ * @return {string}
+ */
+ _computeCommentsStringMobile(changeComments, patchNum, path) {
+ const commentCount = changeComments.computeCommentCount(patchNum, path);
+ return GrCountStringFormatter.computeShortString(commentCount, 'c');
},
_computeReviewed(file, _reviewed) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index cb463c0..5a33c01 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -348,6 +348,135 @@
}
});
+ test('comment filtering', () => {
+ element.changeComments._comments = {
+ '/COMMIT_MSG': [
+ {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
+ {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
+ {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
+ ],
+ 'myfile.txt': [
+ {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
+ {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
+ {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
+ ],
+ 'unresolved.file': [
+ {
+ patch_set: 2,
+ message: 'wat!?',
+ updated: '2017-02-09 16:40:49',
+ id: '1',
+ unresolved: true,
+ },
+ {
+ patch_set: 2,
+ message: 'hi',
+ updated: '2017-02-10 16:40:49',
+ id: '2',
+ in_reply_to: '1',
+ unresolved: false,
+ },
+ {
+ patch_set: 2,
+ message: 'good news!',
+ updated: '2017-02-08 16:40:49',
+ id: '3',
+ unresolved: true,
+ },
+ ],
+ };
+ element.changeComments._drafts = {
+ '/COMMIT_MSG': [
+ {
+ patch_set: 1,
+ message: 'hi',
+ updated: '2017-02-15 16:40:49',
+ id: '5',
+ unresolved: true,
+ },
+ {
+ patch_set: 1,
+ message: 'fyi',
+ updated: '2017-02-15 16:40:49',
+ id: '6',
+ unresolved: false,
+ },
+ ],
+ 'unresolved.file': [
+ {
+ patch_set: 1,
+ message: 'hi',
+ updated: '2017-02-11 16:40:49',
+ id: '4',
+ unresolved: false,
+ },
+ ],
+ };
+
+ assert.equal(
+ element._computeCommentsString(element.changeComments, '1',
+ '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, '1'
+ , '/COMMIT_MSG'), '2c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, '1',
+ 'unresolved.file'), '1 draft');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, '1',
+ 'unresolved.file'), '1d');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, '1',
+ 'myfile.txt', 'comment'), '1 comment');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, '1',
+ 'myfile.txt'), '1c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, '1',
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, '1',
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, '1',
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, '1',
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, '1',
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, '1',
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, '2',
+ '/COMMIT_MSG', 'comment'), '1 comment');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, '2',
+ '/COMMIT_MSG'), '1c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, '1',
+ '/COMMIT_MSG'), '2 drafts');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, '1',
+ '/COMMIT_MSG'), '2d');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, '2',
+ 'myfile.txt', 'comment'), '2 comments');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, '2',
+ 'myfile.txt'), '2c');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, '2',
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, '2',
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(element._computeCommentsString(element.changeComments, '2',
+ 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+ });
+
suite('keyboard shortcuts', () => {
setup(() => {
element._files = [
@@ -526,119 +655,6 @@
});
});
- test('comment filtering', () => {
- const comments = {
- '/COMMIT_MSG': [
- {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
- {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
- {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
- ],
- 'myfile.txt': [
- {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
- {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
- {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
- ],
- 'unresolved.file': [
- {
- patch_set: 2,
- message: 'wat!?',
- updated: '2017-02-09 16:40:49',
- id: '1',
- unresolved: true,
- },
- {
- patch_set: 2,
- message: 'hi',
- updated: '2017-02-10 16:40:49',
- id: '2',
- in_reply_to: '1',
- unresolved: false,
- },
- {
- patch_set: 2,
- message: 'good news!',
- updated: '2017-02-08 16:40:49',
- id: '3',
- unresolved: true,
- },
- ],
- };
- const drafts = {
- 'unresolved.file': [
- {
- patch_set: 2,
- message: 'hi',
- updated: '2017-02-11 16:40:49',
- id: '4',
- in_reply_to: '3',
- unresolved: false,
- },
- ],
- };
- assert.equal(
- element._computeCountString(comments, '1', '/COMMIT_MSG', 'comment'),
- '2 comments');
- assert.equal(
- element._computeCommentsStringMobile(comments, '1', '/COMMIT_MSG'),
- '2c');
- assert.equal(
- element._computeDraftsStringMobile(comments, '1', '/COMMIT_MSG'),
- '2d');
- assert.equal(
- element._computeCountString(comments, '1', 'myfile.txt', 'comment'),
- '1 comment');
- assert.equal(
- element._computeCommentsStringMobile(comments, '1', 'myfile.txt'),
- '1c');
- assert.equal(
- element._computeDraftsStringMobile(comments, '1', 'myfile.txt'),
- '1d');
- assert.equal(
- element._computeCountString(comments, '1',
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsStringMobile(comments, '1',
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(comments, '1',
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeCountString(comments, '2', '/COMMIT_MSG', 'comment'),
- '1 comment');
- assert.equal(
- element._computeCommentsStringMobile(comments, '2', '/COMMIT_MSG'),
- '1c');
- assert.equal(
- element._computeDraftsStringMobile(comments, '2', '/COMMIT_MSG'),
- '1d');
- assert.equal(
- element._computeCountString(comments, '2', 'myfile.txt', 'comment'),
- '2 comments');
- assert.equal(
- element._computeCommentsStringMobile(comments, '2', 'myfile.txt'),
- '2c');
- assert.equal(
- element._computeDraftsStringMobile(comments, '2', 'myfile.txt'),
- '2d');
- assert.equal(
- element._computeCountString(comments, '2',
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(element._computeCountString(comments, '2',
- 'unresolved.file', 'comment'), '3 comments');
- assert.equal(
- element._computeUnresolvedString(comments, [], 2, 'myfile.txt'), '');
- assert.equal(
- element.computeUnresolvedNum(comments, [], 2, 'myfile.txt'), 0);
- assert.equal(
- element._computeUnresolvedString(comments, [], 2, 'unresolved.file'),
- '(1 unresolved)');
- assert.equal(
- element.computeUnresolvedNum(comments, [], 2, 'unresolved.file'), 1);
- assert.equal(
- element._computeUnresolvedString(comments, drafts, 2,
- 'unresolved.file'), '');
- });
-
test('computed properties', () => {
assert.equal(element._computeFileStatus('A'), 'A');
assert.equal(element._computeFileStatus(undefined), 'M');
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index e04eeb7..e93e710 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -93,6 +93,12 @@
gr-account-chip {
display: inline;
}
+ gr-button {
+ --gr-button: {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
.collapsed gr-comment-list,
.collapsed .replyContainer,
.collapsed .hideOnCollapsed,
@@ -159,7 +165,7 @@
no-trailing-margin
class="message hideOnCollapsed"
content="[[message.message]]"
- config="[[_commentLinks]]"></gr-formatted-text>
+ config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
<div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
<gr-button link small on-tap="_handleReplyTap">Reply</gr-button>
</div>
@@ -168,7 +174,7 @@
change-num="[[changeNum]]"
patch-num="[[message._revision_number]]"
project-name="[[projectName]]"
- comment-links="[[_commentLinks]]"></gr-comment-list>
+ project-config="[[_projectConfig]]"></gr-comment-list>
</div>
</template>
<template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index d907f3b..6b4499f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -79,11 +79,10 @@
type: String,
observer: '_projectNameChanged',
},
- _commentLinks: Object,
/**
* @type {{ commentlinks: Array }}
*/
- projectConfig: Object,
+ _projectConfig: Object,
// Computed property needed to trigger Polymer value observing.
_expanded: {
type: Object,
@@ -239,7 +238,7 @@
_projectNameChanged(name) {
this.$.restAPI.getProjectConfig(name).then(config => {
- this._commentLinks = config.commentlinks;
+ this._projectConfig = config;
});
},
});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index 401aaf8..db36438 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -57,6 +57,7 @@
h4:before,
section div:before {
content: ' ';
+ flex-shrink: 0;
width: 1.2em
}
.relatedChanges a {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 780dca2..0143196 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -76,6 +76,12 @@
clear() {
this.loading = true;
this.hidden = true;
+
+ this._relatedResponse = {changes: []};
+ this._submittedTogether = [];
+ this._conflicts = [];
+ this._cherryPicks = [];
+ this._sameTopic = [];
},
reload() {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index df4391e..c035b44 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -337,12 +337,37 @@
true);
});
- test('clear hides', () => {
+ test('clear and empties', () => {
+ const changes = [{
+ project: 'foo/bar',
+ change_id: 'Ideadbeef',
+ commit: {
+ commit: 'deadbeef',
+ parents: [{commit: 'abc123'}],
+ author: {},
+ subject: 'do that thing',
+ },
+ _change_number: 12345,
+ _revision_number: 1,
+ _current_revision_number: 1,
+ status: 'NEW',
+ }];
+ element._relatedResponse = {changes};
+ element._submittedTogether = changes;
+ element._conflicts = changes;
+ element._cherryPicks = changes;
+ element._sameTopic = changes;
+
element.loading = false;
element.hidden = false;
element.clear();
assert.isTrue(element.loading);
assert.isTrue(element.hidden);
+ assert.equal(element._relatedResponse.changes.length, 0);
+ assert.equal(element._submittedTogether.length, 0);
+ assert.equal(element._conflicts.length, 0);
+ assert.equal(element._cherryPicks.length, 0);
+ assert.equal(element._sameTopic.length, 0);
});
test('update fires', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index babd95c..22b53c8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -86,7 +86,8 @@
],
};
element.serverConfig = {note_db_enabled: true};
- sandbox.stub(element, 'fetchIsLatestKnown', () => Promise.resolve(true));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
};
setup(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index a1719da..6fabe89 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -237,16 +237,14 @@
open(opt_focusTarget) {
this.knownLatestState = LatestPatchState.CHECKING;
- this.fetchIsLatestKnown(this.change, this.$.restAPI)
- .then(isUpToDate => {
- this.knownLatestState = isUpToDate ?
+ this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ this.knownLatestState = result.isLatest ?
LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
});
this._focusOn(opt_focusTarget);
- if (!this.draft || !this.draft.length) {
- this.draft = this._loadStoredDraft();
- }
+ this.draft = this._loadStoredDraft();
if (this.$.restAPI.hasPendingDiffDrafts()) {
this._savingComments = true;
this.$.restAPI.awaitPendingDiffDrafts().then(() => {
@@ -510,7 +508,8 @@
},
_focusOn(section) {
- if (section === FocusTarget.ANY) {
+ // Safeguard- always want to focus on something.
+ if (!section || section === FocusTarget.ANY) {
section = this._chooseFocusTarget();
}
if (section === FocusTarget.BODY) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 278f2c6..b590f85 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -103,8 +103,8 @@
eraseDraftCommentStub = sandbox.stub(element.$.storage,
'eraseDraftComment');
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
// Allow the elements created by dom-repeat to be stamped.
flushAsynchronousOperations();
@@ -415,8 +415,18 @@
assert.equal(element.draft, storedDraft);
});
+ test('gets draft from storage even when text is already present', () => {
+ const storedDraft = 'hello world';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.draft = 'foo bar';
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, storedDraft);
+ });
+
test('blank if no stored draft', () => {
getDraftCommentStub.returns(null);
+ element.draft = 'foo bar';
element.open();
assert.isTrue(getDraftCommentStub.called);
assert.equal(element.draft, '');
@@ -505,6 +515,45 @@
assert.isFalse(filter({group: cc2}));
});
+ test('_focusOn', () => {
+ sandbox.spy(element, '_chooseFocusTarget');
+ element.serverConfig = {note_db_enabled: true};
+ flushAsynchronousOperations();
+ const textareaStub = sandbox.stub(element.$.textarea, 'async');
+ const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
+ 'async');
+ const ccStub = sandbox.stub(element.$$('#ccs').focusStart, 'async');
+ element._focusOn();
+ assert.equal(element._chooseFocusTarget.callCount, 1);
+ assert.deepEqual(textareaStub.callCount, 1);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.ANY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 2);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.BODY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.REVIEWERS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.CCS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 1);
+ });
+
test('_chooseFocusTarget', () => {
element._account = null;
assert.strictEqual(
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 9a5b5ed..76da932 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -54,6 +54,12 @@
.remove {
font-size: .9em;
}
+ gr-button {
+ --gr-button: {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
@media screen and (max-width: 50em), screen and (min-width: 75em) {
gr-account-chip:first-of-type {
margin-top: 0;
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index a935e26..0de1580 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -372,6 +372,10 @@
detail: Gerrit.Nav.GroupDetailView.MEMBERS,
});
},
+
+ getUrlForSettings() {
+ return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS});
+ },
};
})(window);
</script>
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 8bc2a0b..c31855a 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -228,6 +228,8 @@
url = this._generateDiffOrEditUrl(params);
} else if (params.view === Views.GROUP) {
url = this._generateGroupUrl(params);
+ } else if (params.view === Views.SETTINGS) {
+ url = this._generateSettingsUrl(params);
} else {
throw new Error('Can\'t generate');
}
@@ -352,6 +354,14 @@
},
/**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateSettingsUrl(params) {
+ return '/settings';
+ },
+
+ /**
* Given an object of parameters, potentially including a `patchNum` or a
* `basePatchNum` or both, return a string representation of that range. If
* no range is indicated in the params, the empty string is returned.
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index ba7aad3..1ba44eb 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -335,8 +335,9 @@
},
_handleForwardSlashKey(e) {
+ const keyboardEvent = this.getKeyboardEvent(e);
if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
+ (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
e.preventDefault();
this.$.searchInput.focus();
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 303734a..209e8c9 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -76,6 +76,10 @@
ChangeComments.prototype._patchNumEquals =
Gerrit.PatchSetBehavior.patchNumEquals;
+ ChangeComments.prototype._isMergeParent =
+ Gerrit.PatchSetBehavior.isMergeParent;
+ ChangeComments.prototype._getParentIndex =
+ Gerrit.PatchSetBehavior.getParentIndex;
/**
* Get an object mapping file paths to a boolean representing whether that
@@ -299,6 +303,14 @@
* @return {boolean}
*/
ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
+ // If the base of the patch range is a parent of a merge, and the comment
+ // appears on a specific parent then only show the comment if the parent
+ // index of the comment matches that of the range.
+ if (comment.parent && comment.side === PARENT) {
+ return this._isMergeParent(range.basePatchNum) &&
+ comment.parent === this._getParentIndex(range.basePatchNum);
+ }
+
// If the base of the range is the parent of the patch:
if (range.basePatchNum === PARENT &&
comment.side === PARENT &&
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index 79056b1..b6e488f 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -174,6 +174,16 @@
comment.patch_set = 2;
assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
patchRange));
+
+ patchRange.basePatchNum = -2;
+ comment.side = PARENT;
+ comment.parent = 1;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.parent = 2;
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
});
test('_isInRevisionOfPatchRange', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
index 999c883..8cefabb 100644
--- a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
+++ b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
@@ -55,8 +55,8 @@
confirm-label="Delete"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Delete Comment</div>
- <div class="main">
+ <div class="header" slot="header">Delete Comment</div>
+ <div class="main" slot="main">
<label for="messageInput">Enter comment delete reason</label>
<iron-autogrow-textarea
id="messageInput"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 5eb0a5d..62527ca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -333,10 +333,10 @@
confirm-label="Discard"
on-confirm="_handleConfirmDiscard"
on-cancel="_closeConfirmDiscardOverlay">
- <div class="header">
+ <div class="header" slot="header">
Discard comment
</div>
- <div class="main">
+ <div class="main" slot="main">
Are you sure you want to discard this draft comment?
</div>
</gr-confirm-dialog>
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 2490509..03380e4 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
@@ -251,6 +251,22 @@
};
},
+ /**
+ * The only line in which add a comment tooltip is cut off is the first
+ * line. Even if there is a collapsed section, The first visible line is
+ * in the position where the second line would have been, if not for the
+ * collapsed section, so don't need to worry about this case for
+ * positioning the tooltip.
+ */
+ _positionActionBox(actionBox, startLine, range) {
+ if (startLine > 1) {
+ actionBox.placeAbove(range);
+ return;
+ }
+ actionBox.positionBelow = true;
+ actionBox.placeBelow(range);
+ },
+
_handleSelection() {
const normalizedRange = this._getNormalizedRange();
if (!normalizedRange) {
@@ -285,17 +301,18 @@
};
actionBox.side = start.side;
if (start.line === end.line) {
- actionBox.placeAbove(domRange);
+ this._positionActionBox(actionBox, start.line, domRange);
} else if (start.node instanceof Text) {
if (start.column) {
- actionBox.placeAbove(start.node.splitText(start.column));
+ this._positionActionBox(actionBox, start.line,
+ start.node.splitText(start.column));
}
start.node.parentElement.normalize(); // Undo splitText from above.
} else if (start.node.classList.contains('content') &&
- start.node.firstChild) {
- actionBox.placeAbove(start.node.firstChild);
+ start.node.firstChild) {
+ this._positionActionBox(actionBox, start.line, start.node.firstChild);
} else {
- actionBox.placeAbove(start.node);
+ this._positionActionBox(actionBox, start.line, start.node);
}
},
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 b63b9a4..4bbf12b 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
@@ -38,6 +38,27 @@
<table id="diffTable">
<tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ <td class="right lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta">
+ <tr class="diff-row side-by-side" left-type="remove" right-type="add">
+ <td class="left lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;"> </span>quod <hl>Epicurum</hl></div></td>
+ <td class="right lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;"> </span> audiam, sit, quod</div></td>
+ </tr>
+ </tbody>
+
+
+ <tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="138"></td>
<td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
@@ -253,6 +274,7 @@
contentStubs = [];
stub('gr-selection-action-box', {
placeAbove: sandbox.stub(),
+ placeBelow: sandbox.stub(),
});
diff = element.querySelector('#diffTable');
builder = {
@@ -270,9 +292,29 @@
window.getSelection().removeAllRanges();
});
+ test('single first line', () => {
+ const content = stubContent(1, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.$$('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('multiline starting on first line', () => {
+ const startContent = stubContent(1, 'right');
+ const endContent = stubContent(2, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(
+ startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = element.$$('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
test('single line', () => {
const content = stubContent(138, 'left');
+ sandbox.spy(element, '_positionActionBox');
emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.$$('gr-selection-action-box');
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 138,
@@ -281,14 +323,18 @@
endChar: 12,
});
assert.equal(getActionSide(), 'left');
+ assert.notOk(actionBox.positionBelow);
});
test('multiline', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
+ sandbox.spy(element, '_positionActionBox');
emulateSelection(
startContent.firstChild, 10, endContent.lastChild, 7);
assert.isTrue(element.isRangeSelected());
+ const actionBox = element.$$('gr-selection-action-box');
+
assert.deepEqual(getActionRange(), {
startLine: 119,
startChar: 10,
@@ -296,6 +342,7 @@
endChar: 36,
});
assert.equal(getActionSide(), 'right');
+ assert.notOk(actionBox.positionBelow);
});
test('multiple ranges aka firefox implementation', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 1dcf25e..48b6964 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -195,7 +195,7 @@
<div class="jumpToFileContainer">
<gr-dropdown-list
id="dropdown"
- value="[[computeDisplayPath(_path)]]"
+ value="[[_path]]"
on-value-change="_handleFileChange"
items="[[_formattedFiles]]"
initial-count="75">
@@ -221,7 +221,7 @@
<gr-patch-range-select
id="rangeSelect"
change-num="[[_changeNum]]"
- comments="[[_changeComments.comments]]"
+ change-comments="[[_changeComments]]"
patch-num="[[_patchRange.patchNum]]"
base-patch-num="[[_patchRange.basePatchNum]]"
files-weblinks="[[_filesWeblinks]]"
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 cae8cbc..953ae1a 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
@@ -79,7 +79,8 @@
// element for selected a file to view.
_formattedFiles: {
type: Array,
- computed: '_formatFilesForDropdown(_fileList)',
+ computed: '_formatFilesForDropdown(_fileList, _patchRange.patchNum, ' +
+ '_changeComments)',
},
// An sorted array of files, as returned by the rest API.
_fileList: {
@@ -641,19 +642,37 @@
return this._getChangePath(change, patchRangeRecord.base, revisions);
},
- _formatFilesForDropdown(fileList) {
+ _formatFilesForDropdown(fileList, patchNum, changeComments) {
if (!fileList) { return; }
const dropdownContent = [];
for (const path of fileList) {
dropdownContent.push({
text: this.computeDisplayPath(path),
mobileText: this.computeTruncatedPath(path),
- value: this.computeDisplayPath(path),
+ value: path,
+ bottomText: this._computeCommentString(changeComments, patchNum,
+ path),
});
}
return dropdownContent;
},
+ _computeCommentString(changeComments, patchNum, path) {
+ const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
+ path);
+ const commentCount = changeComments.computeCommentCount(patchNum, path);
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ return commentString +
+ // Add a space if both comments and unresolved
+ (commentString && unresolvedString ? ', ' : '') +
+ // Add parentheses around unresolved if it exists.
+ (unresolvedString ? `${unresolvedString}` : '');
+ },
+
_computePrefsButtonHidden(prefs, loggedIn) {
return !loggedIn || !prefs;
},
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 ca06b8d..5e23ea3 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
@@ -315,6 +315,34 @@
assert.isTrue(overlayOpenStub.called);
});
+ test('_computeCommentString', done => {
+ loadCommentSpy = sandbox.spy(element.$.commentAPI, 'loadAll');
+ const path = '/test';
+ element.$.commentAPI.loadAll().then(comments => {
+ const commentCountStub =
+ sandbox.stub(comments, 'computeCommentCount');
+ const unresolvedCountStub =
+ sandbox.stub(comments, 'computeUnresolvedNum');
+ commentCountStub.withArgs(1, path).returns(0);
+ commentCountStub.withArgs(2, path).returns(1);
+ commentCountStub.withArgs(3, path).returns(2);
+ commentCountStub.withArgs(4, path).returns(0);
+ unresolvedCountStub.withArgs(1, path).returns(1);
+ unresolvedCountStub.withArgs(2, path).returns(0);
+ unresolvedCountStub.withArgs(3, path).returns(2);
+ unresolvedCountStub.withArgs(4, path).returns(0);
+
+ assert.equal(element._computeCommentString(comments, 1, path),
+ '1 unresolved');
+ assert.equal(element._computeCommentString(comments, 2, path),
+ '1 comment');
+ assert.equal(element._computeCommentString(comments, 3, path),
+ '2 comments, 2 unresolved');
+ assert.equal(element._computeCommentString(comments, 4, path), '');
+ done();
+ });
+ });
+
suite('url params', () => {
setup(() => {
sandbox.stub(Gerrit.Nav, 'getUrlForDiff', (c, p, pn, bpn) => {
@@ -332,21 +360,37 @@
patchNum: '10',
};
element._change = {_number: 42};
- element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md',
+ '/COMMIT_MSG', '/MERGE_LIST'];
element._path = 'glados.txt';
const expectedFormattedFiles = [
{
text: 'chell.go',
mobileText: 'chell.go',
value: 'chell.go',
+ bottomText: '',
}, {
text: 'glados.txt',
mobileText: 'glados.txt',
value: 'glados.txt',
+ bottomText: '',
}, {
text: 'wheatley.md',
mobileText: 'wheatley.md',
value: 'wheatley.md',
+ bottomText: '',
+ },
+ {
+ text: 'Commit message',
+ mobileText: 'Commit message',
+ value: '/COMMIT_MSG',
+ bottomText: '',
+ },
+ {
+ text: 'Merge list',
+ mobileText: 'Merge list',
+ value: '/MERGE_LIST',
+ bottomText: '',
},
];
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index a11902e..7fd97ad 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -18,7 +18,7 @@
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
-
+<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-patch-range-select">
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 4e7fa38..9ac4451 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -34,21 +34,15 @@
_baseDropdownContent: {
type: Object,
computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
- '_sortedRevisions, comments)',
+ '_sortedRevisions, changeComments)',
},
_patchDropdownContent: {
type: Object,
computed: '_computePatchDropdownContent(availablePatches,' +
- 'basePatchNum, _sortedRevisions, comments)',
+ 'basePatchNum, _sortedRevisions, changeComments)',
},
changeNum: String,
- // In the case of a patch range select (like diff view) comments should
- // be an empty array, so that the patch and base content computed values
- // get triggered.
- comments: {
- type: Object,
- value: () => { return {}; },
- },
+ changeComments: Object,
/** @type {{ meta_a: !Array, meta_b: !Array}} */
filesWeblinks: Object,
patchNum: String,
@@ -64,7 +58,7 @@
behaviors: [Gerrit.PatchSetBehavior],
_computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
- comments) {
+ changeComments) {
const dropdownContent = [];
for (const basePatch of availablePatches) {
const basePatchNum = basePatch.num;
@@ -73,9 +67,9 @@
basePatch.num, patchNum, _sortedRevisions),
triggerText: `Patchset ${basePatchNum}`,
text: `Patchset ${basePatchNum}` +
- this._computePatchSetCommentsString(this.comments, basePatchNum),
- mobileText: this._computeMobileText(basePatchNum, comments,
- _sortedRevisions),
+ this._computePatchSetCommentsString(changeComments, basePatchNum),
+ mobileText: this._computeMobileText(basePatchNum,
+ changeComments, _sortedRevisions),
bottomText: `${this._computePatchSetDescription(
_sortedRevisions, basePatchNum)}`,
value: basePatch.num,
@@ -88,14 +82,14 @@
return dropdownContent;
},
- _computeMobileText(patchNum, comments, revisions) {
+ _computeMobileText(patchNum, changeComments, revisions) {
return `${patchNum}` +
- `${this._computePatchSetCommentsString(this.comments, patchNum)}` +
+ `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
`${this._computePatchSetDescription(revisions, patchNum, true)}`;
},
_computePatchDropdownContent(availablePatches, basePatchNum,
- _sortedRevisions, comments) {
+ _sortedRevisions, changeComments) {
const dropdownContent = [];
for (const patch of availablePatches) {
const patchNum = patch.num;
@@ -106,8 +100,8 @@
patchNum,
text: `${patchNum === 'edit' ? '': 'Patchset '}${patchNum}` +
`${this._computePatchSetCommentsString(
- this.comments, patchNum)}`,
- mobileText: this._computeMobileText(patchNum, comments,
+ changeComments, patchNum)}`,
+ mobileText: this._computeMobileText(patchNum, changeComments,
_sortedRevisions),
bottomText: `${this._computePatchSetDescription(
_sortedRevisions, patchNum)}`,
@@ -151,66 +145,26 @@
this.findSortedIndex(patchNum, sortedRevisions);
},
- // Copied from gr-file-list
- // @todo(beckysiegel) clean up.
- _getCommentsForPath(comments, patchNum, path) {
- return (comments[path] || []).filter(c => {
- return this.patchNumEquals(c.patch_set, patchNum);
- });
- },
- // Copied from gr-file-list
- // @todo(beckysiegel) clean up.
- _computeUnresolvedNum(comments, drafts, patchNum, path) {
- comments = this._getCommentsForPath(comments, patchNum, path);
- drafts = this._getCommentsForPath(drafts, patchNum, path);
- comments = comments.concat(drafts);
+ _computePatchSetCommentsString(changeComments, patchNum) {
+ if (!changeComments) { return; }
- // Create an object where every comment ID is the key of an unresolved
- // comment.
+ const commentCount = changeComments.computeCommentCount(patchNum);
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
- const idMap = comments.reduce((acc, comment) => {
- if (comment.unresolved) {
- acc[comment.id] = true;
- }
- return acc;
- }, {});
+ const unresolvedCount = changeComments.computeUnresolvedNum(patchNum);
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
- // Set false for the comments that are marked as parents.
- for (const comment of comments) {
- idMap[comment.in_reply_to] = false;
+ if (!commentString.length && !unresolvedString.length) {
+ return '';
}
- // The unresolved comments are the comments that still have true.
- const unresolvedLeaves = Object.keys(idMap).filter(key => {
- return idMap[key];
- });
-
- return unresolvedLeaves.length;
- },
-
- _computePatchSetCommentsString(allComments, patchNum) {
- // todo (beckysiegel) get comment strings for diff view also.
- if (!allComments) { return ''; }
- let numComments = 0;
- let numUnresolved = 0;
- for (const file in allComments) {
- if (allComments.hasOwnProperty(file)) {
- numComments += this._getCommentsForPath(
- allComments, patchNum, file).length;
- numUnresolved += this._computeUnresolvedNum(
- allComments, {}, patchNum, file);
- }
- }
- let commentsStr = '';
- if (numComments > 0) {
- commentsStr = ' (' + numComments + ' comments';
- if (numUnresolved > 0) {
- commentsStr += ', ' + numUnresolved + ' unresolved';
- }
- commentsStr += ')';
- }
- return commentsStr;
+ return ` (${commentString}` +
+ // Add a comma + space if both comments and unresolved
+ (commentString && unresolvedString ? ', ' : '') +
+ `${unresolvedString})`;
},
/**
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index cd7fe56..04f83bc 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -23,13 +23,24 @@
<link rel="import" href="../../../test/common-test-setup.html"/>
<script src="../../../bower_components/page/page.js"></script>
+<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-patch-range-select.html">
<script>void(0);</script>
+<dom-module id="comment-api-mock">
+ <template>
+ <gr-patch-range-select id="patchRange" auto
+ change-comments="[[_changeComments]]"></gr-patch-range-select>
+ <gr-comment-api id="commentAPI"></gr-comment-api>
+ </template>
+ <script src="../../diff/gr-comment-api/gr-comment-api-mock.js"></script>
+</dom-module>
+
<test-fixture id="basic">
<template>
- <gr-patch-range-select auto></gr-patch-range-select>
+ <comment-api-mock></comment-api-mock>
</template>
</test-fixture>
@@ -37,10 +48,25 @@
suite('gr-patch-range-select tests', () => {
let element;
let sandbox;
+ let commentApiWrapper;
setup(() => {
- element = fixture('basic');
sandbox = sinon.sandbox.create();
+
+ stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.patchRange;
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initalized.
+ return commentApiWrapper.loadComments();
});
teardown(() => sandbox.restore());
@@ -80,23 +106,12 @@
});
test('_computeBaseDropdownContent', () => {
- const comments = {};
const availablePatches = [
{num: 'edit'},
{num: 3},
{num: 2},
{num: 1},
];
- const revisions = [
- {
- commit: {},
- _number: 2,
- description: 'description',
- },
- {commit: {}},
- {commit: {}},
- {commit: {}},
- ];
const patchNum = 1;
const sortedRevisions = [
{_number: 3},
@@ -143,7 +158,8 @@
},
];
assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
- patchNum, sortedRevisions, revisions, comments), expectedResult);
+ patchNum, sortedRevisions, element.changeComments),
+ expectedResult);
});
test('_computeBaseDropdownContent called when patchNum updates', () => {
@@ -170,34 +186,32 @@
assert.equal(element._computeBaseDropdownContent.callCount, 1);
});
- test('_computeBaseDropdownContent called when comments update', () => {
- element.revisions = [
+ test('_computeBaseDropdownContent called when changeComments update',
+ done => {
+ element.revisions = [
{commit: {}},
{commit: {}},
{commit: {}},
{commit: {}},
- ];
- element.availablePatches = [
+ ];
+ element.availablePatches = [
{num: 'edit'},
{num: 3},
{num: 2},
{num: 1},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computeBaseDropdownContent');
- assert.equal(element._computeBaseDropdownContent.callCount, 0);
- element.set('comments', {
- file: [{
- message: 'test',
- patch_set: 2,
- }],
- });
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computeBaseDropdownContent');
+ assert.equal(element._computeBaseDropdownContent.callCount, 0);
+ commentApiWrapper.loadComments().then().then(() => {
+ assert.equal(element._computeBaseDropdownContent.callCount, 1);
+ done();
+ });
+ });
test('_computePatchDropdownContent called when basePatchNum updates', () => {
element.revisions = [
@@ -222,7 +236,7 @@
assert.equal(element._computePatchDropdownContent.callCount, 1);
});
- test('_computePatchDropdownContent called when comments update', () => {
+ test('_computePatchDropdownContent called when comments update', done => {
element.revisions = [
{commit: {}},
{commit: {}},
@@ -242,17 +256,12 @@
// Should be recomputed for each available patch
sandbox.stub(element, '_computePatchDropdownContent');
assert.equal(element._computePatchDropdownContent.callCount, 0);
- element.set('comments', {
- file: [{
- message: 'test',
- patch_set: 2,
- }],
+ commentApiWrapper.loadComments().then().then(() => {
+ done();
});
- assert.equal(element._computePatchDropdownContent.callCount, 1);
});
test('_computePatchDropdownContent', () => {
- const comments = {};
const availablePatches = [
{num: 'edit'},
{num: 3},
@@ -303,7 +312,8 @@
];
assert.deepEqual(element._computePatchDropdownContent(availablePatches,
- basePatchNum, sortedRevisions, comments), expectedResult);
+ basePatchNum, sortedRevisions, element.changeComments),
+ expectedResult);
});
test('filesWeblinks', () => {
@@ -331,7 +341,7 @@
test('_computePatchSetCommentsString', () => {
// Test string with unresolved comments.
- comments = {
+ element.changeComments._comments = {
foo: [{
id: '27dcee4d_f7b77cfa',
message: 'test',
@@ -351,17 +361,18 @@
abc: [],
};
- assert.equal(element._computePatchSetCommentsString(comments, 1),
- ' (3 comments, 1 unresolved)');
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), ' (3 comments, 1 unresolved)');
// Test string with no unresolved comments.
- delete comments['foo'];
- assert.equal(element._computePatchSetCommentsString(comments, 1),
- ' (2 comments)');
+ delete element.changeComments._comments['foo'];
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), ' (2 comments)');
// Test string with no comments.
- delete comments['bar'];
- assert.equal(element._computePatchSetCommentsString(comments, 1), '');
+ delete element.changeComments._comments['bar'];
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), '');
});
test('patch-range-change fires', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 47db5f0..3993b86 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -17,34 +17,24 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html">
<dom-module id="gr-selection-action-box">
<template>
<style include="shared-styles">
:host {
- --gr-arrow-size: .65em;
-
- background-color: rgba(22, 22, 22, .9);
- border-radius: 3px;
- color: #fff;
cursor: pointer;
font-family: var(--font-family);
- padding: .5em .75em;
position: absolute;
white-space: nowrap;
}
- .arrow {
- border: var(--gr-arrow-size) solid transparent;
- border-top: var(--gr-arrow-size) solid rgba(22, 22, 22, 0.9);
- height: 0;
- left: calc(50% - var(--gr-arrow-size));
- margin-top: .5em;
- position: absolute;
- width: 0;
+ #tooltip {
+ --tooltip-background-color: rgba(22, 22, 22, .9);
}
</style>
- Press <strong>c</strong> to comment.
- <div class="arrow"></div>
+ <gr-tooltip id="tooltip"
+ text="press c to comment"
+ position-below="[[positionBelow]]"></gr-tooltip>
</template>
<script src="gr-selection-action-box.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index c228235..61a6eb2 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -37,6 +37,7 @@
endChar: NaN,
},
},
+ positionBelow: Boolean,
side: {
type: String,
value: '',
@@ -58,7 +59,7 @@
placeAbove(el) {
Polymer.dom.flush();
const rect = this._getTargetBoundingRect(el);
- const boxRect = this.getBoundingClientRect();
+ const boxRect = this.$.tooltip.getBoundingClientRect();
const parentRect = this.parentElement.getBoundingClientRect();
this.style.top =
rect.top - parentRect.top - boxRect.height - 6 + 'px';
@@ -66,6 +67,17 @@
rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
},
+ placeBelow(el) {
+ Polymer.dom.flush();
+ const rect = this._getTargetBoundingRect(el);
+ const boxRect = this.$.tooltip.getBoundingClientRect();
+ const parentRect = this.parentElement.getBoundingClientRect();
+ this.style.top =
+ rect.top - parentRect.top + boxRect.height - 6 + 'px';
+ this.style.left =
+ rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+ },
+
_getTargetBoundingRect(el) {
let rect;
if (el instanceof Text) {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index 8c70772..7fc634c 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -38,15 +38,17 @@
suite('gr-selection-action-box', () => {
let container;
let element;
+ let sandbox;
setup(() => {
container = fixture('basic');
element = container.querySelector('gr-selection-action-box');
- sinon.stub(element, 'fire');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, 'fire');
});
teardown(() => {
- element.fire.restore();
+ sandbox.restore();
});
test('ignores regular keys', () => {
@@ -65,10 +67,10 @@
setup(() => {
e = {
button: 0,
- preventDefault: sinon.stub(),
- stopPropagation: sinon.stub(),
+ preventDefault: sandbox.stub(),
+ stopPropagation: sandbox.stub(),
};
- sinon.stub(element, '_fireCreateComment');
+ sandbox.stub(element, '_fireCreateComment');
});
test('event handled if main button', () => {
@@ -107,20 +109,14 @@
setup(() => {
target = container.querySelector('.target');
- sinon.stub(container, 'getBoundingClientRect').returns(
+ sandbox.stub(container, 'getBoundingClientRect').returns(
{top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
- sinon.stub(element, '_getTargetBoundingRect').returns(
+ sandbox.stub(element, '_getTargetBoundingRect').returns(
{top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
- sinon.stub(element, 'getBoundingClientRect').returns(
+ sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
{width: 10, height: 10});
});
- teardown(() => {
- element.getBoundingClientRect.restore();
- container.getBoundingClientRect.restore();
- element._getTargetBoundingRect.restore();
- });
-
test('placeAbove for Element argument', () => {
element.placeAbove(target);
assert.equal(element.style.top, '25px');
@@ -133,13 +129,24 @@
assert.equal(element.style.left, '72px');
});
+ test('placeBelow for Element argument', () => {
+ element.placeBelow(target);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
+
+ test('placeBelow for Text Node argument', () => {
+ element.placeBelow(target.firstChild);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
+
test('uses document.createRange', () => {
- sinon.spy(document, 'createRange');
+ sandbox.spy(document, 'createRange');
element._getTargetBoundingRect.restore();
- sinon.spy(element, '_getTargetBoundingRect');
+ sandbox.spy(element, '_getTargetBoundingRect');
element.placeAbove(target.firstChild);
assert.isTrue(document.createRange.called);
- document.createRange.restore();
});
});
});
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
index 9eb4974..c769d25 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -85,8 +85,8 @@
confirm-label="Edit"
on-confirm="_handleEditConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Edit a file</div>
- <div class="main">
+ <div class="header" slot="header">Edit a file</div>
+ <div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing or new full file path."
query="[[_query]]"
@@ -100,8 +100,8 @@
confirm-label="Delete"
on-confirm="_handleDeleteConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Delete a file</div>
- <div class="main">
+ <div class="header" slot="header">Delete a file</div>
+ <div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing full file path."
query="[[_query]]"
@@ -115,8 +115,8 @@
confirm-label="Rename"
on-confirm="_handleRenameConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Rename a file</div>
- <div class="main">
+ <div class="header" slot="header">Rename a file</div>
+ <div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing full file path."
query="[[_query]]"
@@ -134,8 +134,8 @@
confirm-label="Restore"
on-confirm="_handleRestoreConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Restore this file?</div>
- <div class="main">
+ <div class="header" slot="header">Restore this file?</div>
+ <div class="main" slot="main">
<input
is="iron-input"
disabled
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index a2a1c0b..461f6c9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -14,6 +14,8 @@
(function() {
'use strict';
+ const INIT_PROPERTIES_TIMEOUT_MS = 10000;
+
Polymer({
is: 'gr-endpoint-decorator',
@@ -40,59 +42,73 @@
_initDecoration(name, plugin) {
const el = document.createElement(name);
- this._initProperties(el, plugin, this.getContentChildren().find(
- el => el.nodeName !== 'GR-ENDPOINT-PARAM'));
- this._appendChild(el);
- return el;
+ return this._initProperties(el, plugin,
+ this.getContentChildren().find(
+ el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
+ .then(el => this._appendChild(el));
},
_initReplacement(name, plugin) {
- this.getContentChildNodes().forEach(node => node.remove());
+ this.getContentChildNodes()
+ .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM')
+ .forEach(node => node.remove());
const el = document.createElement(name);
- this._initProperties(el, plugin);
- this._appendChild(el);
- return el;
+ return this._initProperties(el, plugin).then(
+ el => this._appendChild(el));
},
_getEndpointParams() {
- return Polymer.dom(this).querySelectorAll('gr-endpoint-param').map(el => {
- return {name: el.getAttribute('name'), value: el.value};
- });
+ return Polymer.dom(this).querySelectorAll('gr-endpoint-param');
},
/**
* @param {!Element} el
* @param {!Object} plugin
* @param {!Element=} opt_content
+ * @return {!Promise<Element>}
*/
_initProperties(el, plugin, opt_content) {
el.plugin = plugin;
if (opt_content) {
el.content = opt_content;
}
- for (const {name, value} of this._getEndpointParams()) {
- el[name] = value;
- }
+ const expectProperties = this._getEndpointParams().map(
+ paramEl => plugin.attributeHelper(paramEl).get('value')
+ .then(value => el[paramEl.getAttribute('name')] = value)
+ );
+ const timeout = new Promise(
+ resolve => setTimeout(() => {
+ console.warn(
+ 'Timeout waiting for endpoint properties initialization.' +
+ `plugin ${plugin.getPluginName()}, endpoint ${this.name}`);
+ resolve();
+ }, INIT_PROPERTIES_TIMEOUT_MS));
+ return Promise.race([timeout, Promise.all(expectProperties)])
+ .then(() => el);
},
_appendChild(el) {
- Polymer.dom(this.root).appendChild(el);
+ return Polymer.dom(this.root).appendChild(el);
},
_initModule({moduleName, plugin, type, domHook}) {
- let el;
+ let initPromise;
switch (type) {
case 'decorate':
- el = this._initDecoration(moduleName, plugin);
+ initPromise = this._initDecoration(moduleName, plugin);
break;
case 'replace':
- el = this._initReplacement(moduleName, plugin);
+ initPromise = this._initReplacement(moduleName, plugin);
break;
}
- if (el) {
- domHook.handleInstanceAttached(el);
+ if (!initPromise) {
+ console.warn('Unable to initialize module' +
+ `${moduleName} from ${plugin.getPluginName()}`);
}
- this._domHooks.set(el, domHook);
+ initPromise.then(el => {
+ domHook.handleInstanceAttached(el);
+ this._domHooks.set(el, domHook);
+ });
},
ready() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index c7ab3d9..cfebc95 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -28,48 +28,45 @@
<test-fixture id="basic">
<template>
- <gr-endpoint-decorator name="foo">
- <gr-endpoint-param name="someparam" value="barbar"></gr-endpoint-param>
- </gr-endpoint-decorator>
+ <div>
+ <gr-endpoint-decorator name="first">
+ <gr-endpoint-param name="someparam" value="barbar"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <gr-endpoint-decorator name="second">
+ <gr-endpoint-param name="someparam" value="foofoo"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <gr-endpoint-decorator name="banana">
+ <gr-endpoint-param name="someParam" value="yes"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
</template>
</test-fixture>
<script>
suite('gr-endpoint-decorator', () => {
+ let container;
let sandbox;
- let element;
let plugin;
- let domHookStub;
+ let decorationHook;
+ let replacementHook;
setup(done => {
- Gerrit._endpoints = new GrPluginEndpoints();
-
sandbox = sinon.sandbox.create();
-
- domHookStub = {
- handleInstanceAttached: sandbox.stub(),
- handleInstanceDetached: sandbox.stub(),
- getPublicAPI: () => domHookStub,
- };
- sandbox.stub(
- GrDomHooksManager.prototype, 'getDomHook').returns(domHookStub);
-
- // NB: Order is important.
- Gerrit.install(p => {
- plugin = p;
- plugin.registerCustomComponent('foo', 'some-module');
- plugin.registerCustomComponent('foo', 'other-module', {replace: true});
- plugin.registerCustomComponent('bar', 'some-module');
- }, '0.1', 'http://some/plugin/url.html');
-
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
-
- element = fixture('basic');
- sandbox.stub(element, '_initDecoration').returns({});
- sandbox.stub(element, '_initReplacement').returns({});
- sandbox.stub(element, 'importHref', (url, resolve) => resolve());
-
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ // Since _endpoints are global, must reset state.
+ Gerrit._endpoints = new GrPluginEndpoints();
+ container = fixture('basic');
+ Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
+ hooks = [];
+ // Decoration
+ decorationHook = plugin.registerCustomComponent('first', 'some-module');
+ // Replacement
+ replacementHook = plugin.registerCustomComponent(
+ 'second', 'other-module', {replace: true});
+ // Mimic all plugins loaded.
+ Gerrit._setPluginsCount(0);
flush(done);
});
@@ -77,51 +74,79 @@
sandbox.restore();
});
- test('imports plugin-provided module', () => {
- assert.isTrue(
- element.importHref.calledWith(new URL('http://some/plugin/url.html')));
+ test('imports plugin-provided modules into endpoints', () => {
+ const endpoints =
+ Array.from(container.querySelectorAll('gr-endpoint-decorator'));
+ assert.equal(endpoints.length, 3);
+ endpoints.forEach(element => {
+ assert.isTrue(
+ element._import.calledWith(new URL('http://some/plugin/url.html')));
+ });
});
- test('inits decoration dom hook', () => {
- assert.strictEqual(
- element._initDecoration.lastCall.args[0], 'some-module');
- assert.strictEqual(
- element._initDecoration.lastCall.args[1], plugin);
+ test('decoration', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="first"]');
+ const module = Polymer.dom(element.root).children.find(
+ element => element.nodeName === 'SOME-MODULE');
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'barbar');
+ return decorationHook.getLastAttached().then(element => {
+ assert.strictEqual(element, module);
+ }).then(() => {
+ element.remove();
+ assert.equal(decorationHook.getAllAttached().length, 0);
+ });
});
- test('inits replacement dom hook', () => {
- assert.strictEqual(
- element._initReplacement.lastCall.args[0], 'other-module');
- assert.strictEqual(
- element._initReplacement.lastCall.args[1], plugin);
+ test('replacement', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="second"]');
+ const module = Polymer.dom(element.root).children.find(
+ element => element.nodeName === 'OTHER-MODULE');
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'foofoo');
+ return replacementHook.getLastAttached().then(element => {
+ assert.strictEqual(element, module);
+ }).then(() => {
+ element.remove();
+ assert.equal(replacementHook.getAllAttached().length, 0);
+ });
});
- test('calls dom hook handleInstanceAttached', () => {
- assert.equal(domHookStub.handleInstanceAttached.callCount, 2);
- });
-
- test('calls dom hook handleInstanceDetached', () => {
- element.detached();
- assert.equal(domHookStub.handleInstanceDetached.callCount, 2);
- });
-
- test('installs modules on late registration', done => {
- domHookStub.handleInstanceAttached.reset();
- plugin.registerCustomComponent('foo', 'noob-noob');
+ test('late registration', done => {
+ plugin.registerCustomComponent('banana', 'noob-noob');
flush(() => {
- assert.equal(domHookStub.handleInstanceAttached.callCount, 1);
- assert.strictEqual(
- element._initDecoration.lastCall.args[0], 'noob-noob');
- assert.strictEqual(
- element._initDecoration.lastCall.args[1], plugin);
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const module = Polymer.dom(element.root).children.find(
+ element => element.nodeName === 'NOOB-NOOB');
+ assert.isOk(module);
done();
});
});
- test('params', () => {
- const instance = document.createElement('foo');
- element._initProperties(instance, plugin);
- assert.equal(instance.someparam, 'barbar');
+ test('late param setup', done => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const param = Polymer.dom(element).querySelector('gr-endpoint-param');
+ param['value'] = undefined;
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
+ let module = Polymer.dom(element.root).children.find(
+ element => element.nodeName === 'NOOB-NOOB');
+ // Module waits for param to be defined.
+ assert.isNotOk(module);
+ const value = {abc: 'def'};
+ param.value = value;
+ flush(() => {
+ module = Polymer.dom(element.root).children.find(
+ element => element.nodeName === 'NOOB-NOOB');
+ assert.isOk(module);
+ assert.strictEqual(module['someParam'], value);
+ done();
+ });
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index 5a2ab59..2833654 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -18,7 +18,10 @@
is: 'gr-endpoint-param',
properties: {
name: String,
- value: Object,
+ value: {
+ type: Object,
+ notify: true,
+ },
},
});
})();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index 494b9e8..55164e0 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -44,17 +44,28 @@
date-str="[[_account.registered_on]]"></gr-date-formatter>
</span>
</section>
- <section>
+ <section id="usernameSection">
<span class="title">Username</span>
- <span class="value">[[_account.username]]</span>
+ <span
+ hidden$="[[usernameMutable]]"
+ class="value">[[_username]]</span>
+ <span
+ hidden$="[[!usernameMutable]]"
+ class="value">
+ <input
+ is="iron-input"
+ id="usernameInput"
+ disabled="[[_saving]]"
+ on-keydown="_handleKeydown"
+ bind-value="{{_username}}">
</section>
<section id="nameSection">
<span class="title">Full name</span>
<span
- hidden$="[[mutable]]"
+ hidden$="[[nameMutable]]"
class="value">[[_account.name]]</span>
<span
- hidden$="[[!mutable]]"
+ hidden$="[[!nameMutable]]"
class="value">
<input
is="iron-input"
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 5d7f8a6..a698c71 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -24,21 +24,31 @@
*/
properties: {
- mutable: {
+ usernameMutable: {
type: Boolean,
notify: true,
- computed: '_computeMutable(_serverConfig)',
+ computed: '_computeUsernameMutable(_serverConfig, _account.username)',
+ },
+ nameMutable: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeNameMutable(_serverConfig)',
},
hasUnsavedChanges: {
type: Boolean,
notify: true,
- computed: '_computeHasUnsavedChanges(_hasNameChange, _hasStatusChange)',
+ computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
+ '_hasUsernameChange, _hasStatusChange)',
},
_hasNameChange: {
type: Boolean,
value: false,
},
+ _hasUsernameChange: {
+ type: Boolean,
+ value: false,
+ },
_hasStatusChange: {
type: Boolean,
value: false,
@@ -54,6 +64,10 @@
/** @type {?} */
_account: Object,
_serverConfig: Object,
+ _username: {
+ type: String,
+ observer: '_usernameChanged',
+ },
},
observers: [
@@ -71,7 +85,11 @@
}));
promises.push(this.$.restAPI.getAccount().then(account => {
+ // Provide predefined value for username to trigger computation of
+ // username mutability.
+ account.username = account.username || '';
this._account = account;
+ this._username = account.username;
}));
return Promise.all(promises).then(() => {
@@ -88,6 +106,7 @@
// Set only the fields that have changed.
// Must be done in sequence to avoid race conditions (@see Issue 5721)
return this._maybeSetName()
+ .then(this._maybeSetUsername.bind(this))
.then(this._maybeSetStatus.bind(this))
.then(() => {
this._hasNameChange = false;
@@ -98,9 +117,15 @@
},
_maybeSetName() {
- return this._hasNameChange && this.mutable ?
- this.$.restAPI.setAccountName(this._account.name) :
- Promise.resolve();
+ return this._hasNameChange && this.nameMutable ?
+ this.$.restAPI.setAccountName(this._account.name) :
+ Promise.resolve();
+ },
+
+ _maybeSetUsername() {
+ return this._hasUsernameChange && this.usernameMutable ?
+ this.$.restAPI.setAccountUsername(this._username) :
+ Promise.resolve();
},
_maybeSetStatus() {
@@ -109,11 +134,17 @@
Promise.resolve();
},
- _computeHasUnsavedChanges(name, status) {
- return name || status;
+ _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) {
+ return nameChanged || usernameChanged || statusChanged;
},
- _computeMutable(config) {
+ _computeUsernameMutable(config, username) {
+ // Username may not be changed once it is set.
+ return config.auth.editable_account_fields.includes('USER_NAME') &&
+ !username;
+ },
+
+ _computeNameMutable(config) {
return config.auth.editable_account_fields.includes('FULL_NAME');
},
@@ -122,6 +153,11 @@
this._hasStatusChange = true;
},
+ _usernameChanged() {
+ if (this._loading) { return; }
+ this._hasUsernameChange = true;
+ },
+
_nameChanged() {
if (this._loading) { return; }
this._hasNameChange = true;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index 84fbc09..d27d153 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -84,18 +84,18 @@
assert.equal(valueOf('Username').textContent, account.username);
});
- test('user name render (immutable)', () => {
+ test('full name render (immutable)', () => {
const section = element.$.nameSection;
const displaySpan = section.querySelectorAll('.value')[0];
const inputSpan = section.querySelectorAll('.value')[1];
- assert.isFalse(element.mutable);
+ assert.isFalse(element.nameMutable);
assert.isFalse(displaySpan.hasAttribute('hidden'));
assert.equal(displaySpan.textContent, account.name);
assert.isTrue(inputSpan.hasAttribute('hidden'));
});
- test('user name render (mutable)', () => {
+ test('full name render (mutable)', () => {
element.set('_serverConfig',
{auth: {editable_account_fields: ['FULL_NAME']}});
@@ -103,32 +103,64 @@
const displaySpan = section.querySelectorAll('.value')[0];
const inputSpan = section.querySelectorAll('.value')[1];
- assert.isTrue(element.mutable);
+ assert.isTrue(element.nameMutable);
assert.isTrue(displaySpan.hasAttribute('hidden'));
assert.equal(element.$.nameInput.bindValue, account.name);
assert.isFalse(inputSpan.hasAttribute('hidden'));
});
+ test('username render (immutable)', () => {
+ const section = element.$.usernameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isFalse(element.usernameMutable);
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.username);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+ });
+
+ test('username render (mutable)', () => {
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['USER_NAME']}});
+ element.set('_account.username', '');
+ element.set('_username', '');
+
+ const section = element.$.usernameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isTrue(element.usernameMutable);
+ assert.isTrue(displaySpan.hasAttribute('hidden'));
+ assert.equal(element.$.usernameInput.bindValue, account.username);
+ assert.isFalse(inputSpan.hasAttribute('hidden'));
+ });
+
suite('account info edit', () => {
let nameChangedSpy;
+ let usernameChangedSpy;
let statusChangedSpy;
let nameStub;
+ let usernameStub;
let statusStub;
setup(() => {
nameChangedSpy = sandbox.spy(element, '_nameChanged');
+ usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
statusChangedSpy = sandbox.spy(element, '_statusChanged');
element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME']}});
+ {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
- nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name =>
- Promise.resolve());
+ nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+ name => Promise.resolve());
+ usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
+ username => Promise.resolve());
statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
status => Promise.resolve());
});
test('name', done => {
- assert.isTrue(element.mutable);
+ assert.isTrue(element.nameMutable);
assert.isFalse(element.hasUnsavedChanges);
element.set('_account.name', 'new name');
@@ -137,18 +169,40 @@
assert.isFalse(statusChangedSpy.called);
assert.isTrue(element.hasUnsavedChanges);
- MockInteractions.pressAndReleaseKeyOn(element.$.nameInput, 13);
+ element.save().then(() => {
+ assert.isFalse(usernameStub.called);
+ assert.isTrue(nameStub.called);
+ assert.isFalse(statusStub.called);
+ nameStub.lastCall.returnValue.then(() => {
+ assert.equal(nameStub.lastCall.args[0], 'new name');
+ done();
+ });
+ });
+ });
- assert.isTrue(nameStub.called);
- assert.isFalse(statusStub.called);
- nameStub.lastCall.returnValue.then(() => {
- assert.equal(nameStub.lastCall.args[0], 'new name');
- done();
+ test('username', done => {
+ element.set('_account.username', '');
+ element._hasUsernameChange = false;
+ assert.isTrue(element.usernameMutable);
+
+ element.set('_username', 'new username');
+
+ assert.isTrue(usernameChangedSpy.called);
+ assert.isFalse(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
+
+ element.save().then(() => {
+ assert.isTrue(usernameStub.called);
+ assert.isFalse(nameStub.called);
+ assert.isFalse(statusStub.called);
+ usernameStub.lastCall.returnValue.then(() => {
+ assert.equal(usernameStub.lastCall.args[0], 'new username');
+ done();
+ });
});
});
test('status', done => {
- assert.isTrue(element.mutable);
assert.isFalse(element.hasUnsavedChanges);
element.set('_account.status', 'new status');
@@ -158,6 +212,7 @@
assert.isTrue(element.hasUnsavedChanges);
element.save().then(() => {
+ assert.isFalse(usernameStub.called);
assert.isTrue(statusStub.called);
assert.isFalse(nameStub.called);
statusStub.lastCall.returnValue.then(() => {
@@ -178,16 +233,18 @@
nameChangedSpy = sandbox.spy(element, '_nameChanged');
statusChangedSpy = sandbox.spy(element, '_statusChanged');
element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME']}});
+ {auth: {editable_account_fields: ['FULL_NAME']}});
- nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name =>
- Promise.resolve());
+ nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+ name => Promise.resolve());
statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
status => Promise.resolve());
+ sandbox.stub(element.$.restAPI, 'setAccountUsername',
+ username => Promise.resolve());
});
test('set name and status', done => {
- assert.isTrue(element.mutable);
+ assert.isTrue(element.nameMutable);
assert.isFalse(element.hasUnsavedChanges);
element.set('_account.name', 'new name');
@@ -231,7 +288,7 @@
const displaySpan = section.querySelectorAll('.value')[0];
const inputSpan = section.querySelectorAll('.value')[1];
- assert.isFalse(element.mutable);
+ assert.isFalse(element.nameMutable);
assert.isFalse(element.hasUnsavedChanges);
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 543c86d..26a2470 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -24,7 +24,7 @@
},
_handleMoveUpButton(e) {
- const index = e.target.dataIndex;
+ const index = Polymer.dom(e).localTarget.dataIndex;
if (index === 0) { return; }
const row = this.menuItems[index];
const prev = this.menuItems[index - 1];
@@ -32,7 +32,7 @@
},
_handleMoveDownButton(e) {
- const index = e.target.dataIndex;
+ const index = Polymer.dom(e).localTarget.dataIndex;
if (index === this.menuItems.length - 1) { return; }
const row = this.menuItems[index];
const next = this.menuItems[index + 1];
@@ -40,7 +40,7 @@
},
_handleDeleteButton(e) {
- const index = e.target.dataIndex;
+ const index = Polymer.dom(e).localTarget.dataIndex;
this.splice('menuItems', index, 1);
},
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 f16ba6c..c70ae88 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
@@ -47,9 +47,10 @@
// Click the up/down button (according to direction) for the index'th row.
// 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';
- const button = element.$$('tbody').querySelector(selector);
+ const selector = 'tr:nth-child(' + (index + 1) + ') .move' +
+ direction + 'Button';
+ const button =
+ element.$$('tbody').querySelector(selector).$$('paper-button');
MockInteractions.tap(button);
}
@@ -141,15 +142,15 @@
['first name', 'second name', 'third name']);
// Tap the delete button for the middle item.
- MockInteractions.tap(
- element.$$('tbody').querySelector('tr:nth-child(2) .remove-button'));
+ MockInteractions.tap(element.$$('tbody')
+ .querySelector('tr:nth-child(2) .remove-button').$$('paper-button'));
assertMenuNamesEqual(element, ['first name', 'third name']);
// Delete remaining items.
for (let i = 0; i < 2; i++) {
- MockInteractions.tap(
- element.$$('tbody').querySelector('tr:first-child .remove-button'));
+ MockInteractions.tap(element.$$('tbody')
+ .querySelector('tr:first-child .remove-button').$$('paper-button'));
}
assertMenuNamesEqual(element, []);
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index de6783d..0cbd1f6 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -16,6 +16,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -37,18 +38,23 @@
header {
border-bottom: 1px solid #cdcdcd;
font-family: var(--font-family-bold);
+ margin-bottom: 1em;
}
- header,
- main,
- footer {
+ .container {
padding: .5em 1.5em;
}
footer {
display: flex;
- justify-content: space-between;
+ justify-content: flex-end;
+ }
+ footer gr-button {
+ margin-left: 1em;
+ }
+ input {
+ width: 20em;
}
</style>
- <main class="gr-form-styles">
+ <div class="container gr-form-styles">
<header>Please confirm your contact information</header>
<main>
<p>
@@ -64,8 +70,15 @@
is="iron-input"
id="name"
bind-value="{{_account.name}}"
- disabled="[[_saving]]"
- on-keydown="_handleNameKeydown">
+ disabled="[[_saving]]">
+ </section>
+ <section>
+ <div class="title">Username</div>
+ <input
+ is="iron-input"
+ id="username"
+ bind-value="{{_account.username}}"
+ disabled="[[_saving]]">
</section>
<section>
<div class="title">Preferred Email</div>
@@ -78,19 +91,26 @@
</template>
</select>
</section>
+ <hr>
+ <p>
+ More configuration options for Gerrit may be found in the
+ <a on-tap="close" href$="[[_computeSettingsUrl(_account)]]">settings</a>.
+ </p>
</main>
<footer>
<gr-button
- id="saveButton"
- primary
- disabled="[[_saving]]"
- on-tap="_handleSave">Save</gr-button>
- <gr-button
id="closeButton"
+ link
disabled="[[_saving]]"
on-tap="_handleClose">Close</gr-button>
+ <gr-button
+ id="saveButton"
+ primary
+ link
+ disabled="[[_computeSaveDisabled(_account.name, _account.username, _account.email, _saving)]]"
+ on-tap="_handleSave">Save</gr-button>
</footer>
- </main>
+ </div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-registration-dialog.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 71d80eb..406d16c 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -31,8 +31,18 @@
properties: {
/** @type {?} */
- _account: Object,
- _saving: Boolean,
+ _account: {
+ type: Object,
+ value: () => {
+ // Prepopulate possibly undefined fields with values to trigger
+ // computed bindings.
+ return {email: null, name: null, username: null};
+ },
+ },
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
},
hostAttributes: {
@@ -41,22 +51,19 @@
attached() {
this.$.restAPI.getAccount().then(account => {
- this._account = account;
+ // Using Object.assign here allows preservation of the default values
+ // supplied in the value generating function of this._account, unless
+ // they are overridden by properties in the account from the response.
+ this._account = Object.assign({}, this._account, account);
});
},
- _handleNameKeydown(e) {
- if (e.keyCode === 13) { // Enter
- e.stopPropagation();
- this._save();
- }
- },
-
_save() {
this._saving = true;
const promises = [
this.$.restAPI.setAccountName(this.$.name.value),
- this.$.restAPI.setPreferredAccountEmail(this.$.email.value),
+ this.$.restAPI.setAccountUsername(this.$.username.value),
+ this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''),
];
return Promise.all(promises).then(() => {
this._saving = false;
@@ -66,15 +73,25 @@
_handleSave(e) {
e.preventDefault();
- this._save().then(() => {
- this.fire('close');
- });
+ this._save().then(this.close.bind(this));
},
_handleClose(e) {
e.preventDefault();
+ this.close();
+ },
+
+ close() {
this._saving = true; // disable buttons indefinitely
this.fire('close');
},
+
+ _computeSaveDisabled(name, username, email, saving) {
+ return !name || !username || !email || saving;
+ },
+
+ _computeSettingsUrl() {
+ return Gerrit.Nav.getUrlForSettings();
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index 858c3ae..15b4fa2 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -41,13 +41,16 @@
suite('gr-registration-dialog tests', () => {
let element;
let account;
+ let sandbox;
let _listeners;
setup(done => {
+ sandbox = sinon.sandbox.create();
_listeners = {};
account = {
name: 'name',
+ username: 'username',
email: 'email',
secondary_emails: [
'email2',
@@ -65,6 +68,10 @@
account.name = name;
return Promise.resolve();
},
+ setAccountUsername(username) {
+ account.username = username;
+ return Promise.resolve();
+ },
setPreferredAccountEmail(email) {
account.email = email;
return Promise.resolve();
@@ -75,6 +82,7 @@
});
teardown(() => {
+ sandbox.restore();
for (const eventType in _listeners) {
if (_listeners.hasOwnProperty(eventType)) {
element.removeEventListener(eventType, _listeners[eventType]);
@@ -119,32 +127,26 @@
}).then(done);
});
- test('saves name and preferred email', done => {
+ test('saves account details', done => {
flush(() => {
element.$.name.value = 'new name';
+ element.$.username.value = 'new username';
element.$.email.value = 'email3';
// Nothing should be committed yet.
assert.equal(account.name, 'name');
+ assert.equal(account.username, 'username');
assert.equal(account.email, 'email');
// Save and verify new values are committed.
save().then(() => {
assert.equal(account.name, 'new name');
+ assert.equal(account.username, 'new username');
assert.equal(account.email, 'email3');
}).then(done);
});
});
- test('pressing enter saves name', done => {
- element.$.name.value = 'entered name';
- save(() => {
- MockInteractions.pressAndReleaseKeyOn(element.$.name, 13); // 'enter'
- }).then(() => {
- assert.equal(account.name, 'entered name');
- }).then(done);
- });
-
test('email select properly populated', done => {
element._account = {email: 'foo', secondary_emails: ['bar', 'baz']};
flush(() => {
@@ -152,5 +154,15 @@
done();
});
});
+
+ test('save btn disabled', () => {
+ const compute = element._computeSaveDisabled;
+ assert.isTrue(compute('', '', '', false));
+ assert.isTrue(compute('', 'test', 'test', false));
+ assert.isTrue(compute('test', '', 'test', false));
+ assert.isTrue(compute('test', 'test', '', false));
+ assert.isTrue(compute('test', 'test', 'test', true));
+ assert.isFalse(compute('test', 'test', 'test', false));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index 51fa616..cc35e08 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -56,6 +56,9 @@
font-family: var(--font-family-bold);
margin-left: 1em;
text-decoration: none;
+ --gr-button: {
+ padding: 0;
+ }
}
</style>
<span class="text">[[text]]</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 6918f30..a56394e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -75,7 +75,7 @@
/* styles for raised buttons specifically*/
:host([primary]) paper-button[raised],
:host([secondary]) paper-button[raised] {
- background-color: var(--gr-button-background, --color-link);
+ background-color: var(--color-link);
color: #fff;
}
:host([primary]) paper-button[raised]:hover,
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
index efa2b35..6801dea 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -55,10 +55,10 @@
}
</style>
<div class="container">
- <header><content select=".header"></content></header>
- <main><content select=".main"></content></main>
+ <header><slot name="header"></slot></header>
+ <main><slot name="main"></slot></main>
<footer>
- <gr-button link on-tap="_handleCancelTap">Cancel</gr-button>
+ <gr-button link on-tap="_handleCancelTap">[[cancelLabel]]</gr-button>
<gr-button link primary on-tap="_handleConfirmTap" disabled="[[disabled]]">
[[confirmLabel]]
</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
index 3d5e781..f322e25 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -34,6 +34,10 @@
type: String,
value: 'Confirm',
},
+ cancelLabel: {
+ type: String,
+ value: 'Cancel',
+ },
disabled: {
type: Boolean,
value: false,
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.html
new file mode 100644
index 0000000..832747e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.html
@@ -0,0 +1,57 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<script>
+ (function(window) {
+ 'use strict';
+ const GrCountStringFormatter = window.GrCountStringFormatter || {};
+
+ /**
+ * Returns a count plus string that is pluralized when necessary.
+ *
+ * @param {number} count
+ * @param {string} noun
+ * @return {string}
+ */
+ GrCountStringFormatter.computePluralString = function(count, noun) {
+ return this.computeString(count, noun) + (count > 1 ? 's' : '');
+ };
+
+ /**
+ * Returns a count plus string that is not pluralized.
+ *
+ * @param {number} count
+ * @param {string} noun
+ * @return {string}
+ */
+ GrCountStringFormatter.computeString = function(count, noun) {
+ if (count === 0) { return ''; }
+ return count + ' ' + noun;
+ };
+
+ /**
+ * Returns a count plus arbitrary text.
+ *
+ * @param {number} count
+ * @param {string} text
+ * @return {string}
+ */
+ GrCountStringFormatter.computeShortString = function(count, text) {
+ if (count === 0) { return ''; }
+ return count + text;
+ };
+ window.GrCountStringFormatter = GrCountStringFormatter;
+ })(window);
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
new file mode 100644
index 0000000..f33db80
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -0,0 +1,54 @@
+<!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-count-string-formatter</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-count-string-formatter.html"/>
+
+<script>
+ suite('gr-count-string-formatter tests', () => {
+ test('computeString', () => {
+ const noun = 'unresolved';
+ assert.equal(GrCountStringFormatter.computeString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computeString(1, noun),
+ '1 unresolved');
+ assert.equal(GrCountStringFormatter.computeString(2, noun),
+ '2 unresolved');
+ });
+
+ test('computeShortString', () => {
+ const noun = 'c';
+ assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
+ assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
+ });
+
+ test('computePluralString', () => {
+ const noun = 'comment';
+ assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computePluralString(1, noun),
+ '1 comment');
+ assert.equal(GrCountStringFormatter.computePluralString(2, noun),
+ '2 comments');
+ });
+ });
+</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index 6853a41..fe74906 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -14,21 +14,49 @@
(function(window) {
'use strict';
+ /**
+ * Ensure GrChangeActionsInterface instance has access to gr-change-actions
+ * element and retrieve if the interface was created before element.
+ * @param {!GrChangeActionsInterface} api
+ */
+ function ensureEl(api) {
+ if (!api._el) {
+ const sharedApiElement = document.createElement('gr-js-api-interface');
+ setEl(api, sharedApiElement.getElement(
+ sharedApiElement.Element.CHANGE_ACTIONS));
+ }
+ }
+
+ /**
+ * Set gr-change-actions element to a GrChangeActionsInterface instance.
+ * @param {!GrChangeActionsInterface} api
+ * @param {!Element} el gr-change-actions
+ */
+ function setEl(api, el) {
+ if (!el) {
+ console.warn('changeActions() is not ready');
+ return;
+ }
+ api._el = el;
+ api.RevisionActions = el.RevisionActions;
+ api.ChangeActions = el.ChangeActions;
+ api.ActionType = el.ActionType;
+ }
+
function GrChangeActionsInterface(plugin, el) {
this.plugin = plugin;
- this._el = el;
- this.RevisionActions = el.RevisionActions;
- this.ChangeActions = el.ChangeActions;
- this.ActionType = el.ActionType;
+ setEl(this, el);
}
GrChangeActionsInterface.prototype.addPrimaryActionKey = function(key) {
+ ensureEl(this);
if (this._el.primaryActionKeys.includes(key)) { return; }
this._el.push('primaryActionKeys', key);
};
GrChangeActionsInterface.prototype.removePrimaryActionKey = function(key) {
+ ensureEl(this);
this._el.primaryActionKeys = this._el.primaryActionKeys.filter(k => {
return k !== key;
});
@@ -36,45 +64,55 @@
GrChangeActionsInterface.prototype.setActionOverflow = function(type, key,
overflow) {
+ ensureEl(this);
return this._el.setActionOverflow(type, key, overflow);
};
GrChangeActionsInterface.prototype.setActionPriority = function(type, key,
priority) {
+ ensureEl(this);
return this._el.setActionPriority(type, key, priority);
};
GrChangeActionsInterface.prototype.setActionHidden = function(type, key,
hidden) {
+ ensureEl(this);
return this._el.setActionHidden(type, key, hidden);
};
GrChangeActionsInterface.prototype.add = function(type, label) {
+ ensureEl(this);
return this._el.addActionButton(type, label);
};
GrChangeActionsInterface.prototype.remove = function(key) {
+ ensureEl(this);
return this._el.removeActionButton(key);
};
GrChangeActionsInterface.prototype.addTapListener = function(key, handler) {
+ ensureEl(this);
this._el.addEventListener(key + '-tap', handler);
};
GrChangeActionsInterface.prototype.removeTapListener = function(key,
handler) {
+ ensureEl(this);
this._el.removeEventListener(key + '-tap', handler);
};
GrChangeActionsInterface.prototype.setLabel = function(key, text) {
+ ensureEl(this);
this._el.setActionButtonProp(key, 'label', text);
};
GrChangeActionsInterface.prototype.setEnabled = function(key, enabled) {
+ ensureEl(this);
this._el.setActionButtonProp(key, 'enabled', enabled);
};
GrChangeActionsInterface.prototype.getActionDetails = function(action) {
+ ensureEl(this);
return this._el.getActionDetails(action) ||
this._el.getActionDetails(this.plugin.getPluginName() + '~' + action);
};
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 53d7345..21f7629 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -39,6 +39,7 @@
suite('gr-js-api-interface tests', () => {
let element;
let changeActions;
+ let plugin;
// Because deepEqual doesn’t behave in Safari.
function assertArraysEqual(actual, expected) {
@@ -48,132 +49,152 @@
}
}
- setup(() => {
- element = fixture('basic');
- element.change = {};
- element._hasKnownChainState = false;
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeActions = plugin.changeActions();
- });
+ suite('early init', () => {
+ setup(() => {
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeActions = plugin.changeActions();
+ element = fixture('basic');
+ });
- teardown(() => {
- changeActions = null;
- });
+ teardown(() => {
+ changeActions = null;
+ });
- test('property existence', () => {
- const properties = [
- 'ActionType',
- 'ChangeActions',
- 'RevisionActions',
- ];
- for (const p of properties) {
- assertArraysEqual(changeActions[p], element[p]);
- }
- });
-
- test('add/remove primary action keys', () => {
- element.primaryActionKeys = [];
- changeActions.addPrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['foo']);
- changeActions.addPrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['foo']);
- changeActions.addPrimaryActionKey('bar');
- assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
- changeActions.removePrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['bar']);
- changeActions.removePrimaryActionKey('baz');
- assertArraysEqual(element.primaryActionKeys, ['bar']);
- changeActions.removePrimaryActionKey('bar');
- assertArraysEqual(element.primaryActionKeys, []);
- });
-
- test('action buttons', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- const handler = sinon.spy();
- changeActions.addTapListener(key, handler);
- flush(() => {
- MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
- assert(handler.calledOnce);
- changeActions.removeTapListener(key, handler);
- MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
- assert(handler.calledOnce);
- changeActions.remove(key);
- flush(() => {
- assert.isNull(element.$$('[data-action-key="' + key + '"]'));
- done();
+ test('does not throw', ()=> {
+ assert.doesNotThrow(() => {
+ changeActions.add('change', 'foo');
});
});
});
- test('action button properties', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- flush(() => {
- const button = element.$$('[data-action-key="' + key + '"]');
- assert.isOk(button);
- assert.equal(button.getAttribute('data-label'), 'Bork!');
- assert.isNotOk(button.disabled);
- changeActions.setLabel(key, 'Yo');
- changeActions.setEnabled(key, false);
+ suite('normal init', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.change = {};
+ element._hasKnownChainState = false;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeActions = plugin.changeActions();
+ });
+
+ teardown(() => {
+ changeActions = null;
+ });
+
+ test('property existence', () => {
+ const properties = [
+ 'ActionType',
+ 'ChangeActions',
+ 'RevisionActions',
+ ];
+ for (const p of properties) {
+ assertArraysEqual(changeActions[p], element[p]);
+ }
+ });
+
+ test('add/remove primary action keys', () => {
+ element.primaryActionKeys = [];
+ changeActions.addPrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['foo']);
+ changeActions.addPrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['foo']);
+ changeActions.addPrimaryActionKey('bar');
+ assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
+ changeActions.removePrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['bar']);
+ changeActions.removePrimaryActionKey('baz');
+ assertArraysEqual(element.primaryActionKeys, ['bar']);
+ changeActions.removePrimaryActionKey('bar');
+ assertArraysEqual(element.primaryActionKeys, []);
+ });
+
+ test('action buttons', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ const handler = sinon.spy();
+ changeActions.addTapListener(key, handler);
flush(() => {
- assert.equal(button.getAttribute('data-label'), 'Yo');
- assert.isTrue(button.disabled);
- done();
+ MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
+ assert(handler.calledOnce);
+ changeActions.removeTapListener(key, handler);
+ MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
+ assert(handler.calledOnce);
+ changeActions.remove(key);
+ flush(() => {
+ assert.isNull(element.$$('[data-action-key="' + key + '"]'));
+ done();
+ });
});
});
- });
- test('hide action buttons', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- flush(() => {
- const button = element.$$('[data-action-key="' + key + '"]');
- assert.isOk(button);
- assert.isFalse(button.hasAttribute('hidden'));
- changeActions.setActionHidden(
- changeActions.ActionType.REVISION, key, true);
+ test('action button properties', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
flush(() => {
const button = element.$$('[data-action-key="' + key + '"]');
- assert.isNotOk(button);
- done();
+ assert.isOk(button);
+ assert.equal(button.getAttribute('data-label'), 'Bork!');
+ assert.isNotOk(button.disabled);
+ changeActions.setLabel(key, 'Yo');
+ changeActions.setEnabled(key, false);
+ flush(() => {
+ assert.equal(button.getAttribute('data-label'), 'Yo');
+ assert.isTrue(button.disabled);
+ done();
+ });
});
});
- });
- test('move action button to overflow', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- flush(() => {
- assert.isTrue(element.$.moreActions.hidden);
- assert.isOk(element.$$('[data-action-key="' + key + '"]'));
- changeActions.setActionOverflow(
- changeActions.ActionType.REVISION, key, true);
+ test('hide action buttons', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
flush(() => {
- assert.isNotOk(element.$$('[data-action-key="' + key + '"]'));
- assert.isFalse(element.$.moreActions.hidden);
- assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
- done();
+ const button = element.$$('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.isFalse(button.hasAttribute('hidden'));
+ changeActions.setActionHidden(
+ changeActions.ActionType.REVISION, key, true);
+ flush(() => {
+ const button = element.$$('[data-action-key="' + key + '"]');
+ assert.isNotOk(button);
+ done();
+ });
});
});
- });
- test('change actions priority', done => {
- const key1 =
+ test('move action button to overflow', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ assert.isTrue(element.$.moreActions.hidden);
+ assert.isOk(element.$$('[data-action-key="' + key + '"]'));
+ changeActions.setActionOverflow(
+ changeActions.ActionType.REVISION, key, true);
+ flush(() => {
+ assert.isNotOk(element.$$('[data-action-key="' + key + '"]'));
+ assert.isFalse(element.$.moreActions.hidden);
+ assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
+ done();
+ });
+ });
+ });
+
+ test('change actions priority', done => {
+ const key1 =
changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- const key2 =
+ const key2 =
changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?');
- flush(() => {
- let buttons =
- Polymer.dom(element.root).querySelectorAll('[data-action-key]');
- assert.equal(buttons[0].getAttribute('data-action-key'), key1);
- assert.equal(buttons[1].getAttribute('data-action-key'), key2);
- changeActions.setActionPriority(
- changeActions.ActionType.REVISION, key1, 10);
flush(() => {
- buttons =
+ let buttons =
+ Polymer.dom(element.root).querySelectorAll('[data-action-key]');
+ assert.equal(buttons[0].getAttribute('data-action-key'), key1);
+ assert.equal(buttons[1].getAttribute('data-action-key'), key2);
+ changeActions.setActionPriority(
+ changeActions.ActionType.REVISION, key1, 10);
+ flush(() => {
+ buttons =
Polymer.dom(element.root).querySelectorAll('[data-action-key]');
- assert.equal(buttons[0].getAttribute('data-action-key'), key2);
- assert.equal(buttons[1].getAttribute('data-action-key'), key1);
- done();
+ assert.equal(buttons[0].getAttribute('data-action-key'), key2);
+ assert.equal(buttons[1].getAttribute('data-action-key'), key1);
+ done();
+ });
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index 65aa364..adaf622 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -15,6 +15,19 @@
'use strict';
/**
+ * Ensure GrChangeReplyInterface instance has access to gr-reply-dialog
+ * element and retrieve if the interface was created before element.
+ * @param {!GrChangeReplyInterfaceOld} api
+ */
+ function ensureEl(api) {
+ if (!api._el) {
+ const sharedApiElement = document.createElement('gr-js-api-interface');
+ api._el = sharedApiElement.getElement(
+ sharedApiElement.Element.REPLY_DIALOG);
+ }
+ }
+
+ /**
* @deprecated
*/
function GrChangeReplyInterfaceOld(el) {
@@ -22,14 +35,17 @@
}
GrChangeReplyInterfaceOld.prototype.getLabelValue = function(label) {
+ ensureEl(this);
return this._el.getLabelValue(label);
};
GrChangeReplyInterfaceOld.prototype.setLabelValue = function(label, value) {
+ ensureEl(this);
this._el.setLabelValue(label, value);
};
GrChangeReplyInterfaceOld.prototype.send = function(opt_includeComments) {
+ ensureEl(this);
return this._el.send(opt_includeComments);
};
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index 2f67035..e453fd2 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -40,37 +40,72 @@
let element;
let sandbox;
let changeReply;
+ let plugin;
setup(() => {
+ sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
getAccount() { return Promise.resolve(null); },
});
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeReply = plugin.changeReply();
});
teardown(() => {
- changeReply = null;
sandbox.restore();
});
- test('calls', () => {
- sandbox.stub(element, 'getLabelValue').returns('+123');
- assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+ suite('early init', () => {
+ setup(() => {
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeReply = plugin.changeReply();
+ element = fixture('basic');
+ });
- sandbox.stub(element, 'setLabelValue');
- changeReply.setLabelValue('My-Label', '+1337');
- assert.isTrue(
- element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+ teardown(() => {
+ changeReply = null;
+ });
- sandbox.stub(element, 'send');
- changeReply.send(false);
- assert.isTrue(element.send.calledWithExactly(false));
+ test('works', () => {
+ sandbox.stub(element, 'getLabelValue').returns('+123');
+ assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+
+ sandbox.stub(element, 'setLabelValue');
+ changeReply.setLabelValue('My-Label', '+1337');
+ assert.isTrue(
+ element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+
+ sandbox.stub(element, 'send');
+ changeReply.send(false);
+ assert.isTrue(element.send.calledWithExactly(false));
+ });
+ });
+
+ suite('normal init', () => {
+ setup(() => {
+ element = fixture('basic');
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeReply = plugin.changeReply();
+ });
+
+ teardown(() => {
+ changeReply = null;
+ });
+
+ test('works', () => {
+ sandbox.stub(element, 'getLabelValue').returns('+123');
+ assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+
+ sandbox.stub(element, 'setLabelValue');
+ changeReply.setLabelValue('My-Label', '+1337');
+ assert.isTrue(
+ element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+
+ sandbox.stub(element, 'send');
+ changeReply.send(false);
+ assert.isTrue(element.send.calledWithExactly(false));
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index d30bad2..6159d22 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -68,6 +68,9 @@
opacity: .6;
pointer-events: none;
}
+ a {
+ color: var(--linked-chip-text-color);
+ }
</style>
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
<a href$="[[href]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 5afed95..0ba6f97 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -16,6 +16,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-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="gr-etag-decorator.html">
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 dcdeb91..45def55 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
@@ -14,6 +14,16 @@
(function() {
'use strict';
+ const Defs = {};
+
+ /**
+ * @typedef {{
+ * basePatchNum: (string|number),
+ * patchNum: (number),
+ * }}
+ */
+ Defs.patchRange;
+
const DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
@@ -30,11 +40,17 @@
SEND_DIFF_DRAFT: 'sendDiffDraft',
};
+ const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
+ 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
+ const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+
+
Polymer({
is: 'gr-rest-api-interface',
behaviors: [
Gerrit.PathListBehavior,
+ Gerrit.PatchSetBehavior,
Gerrit.RESTClientBehavior,
],
@@ -553,25 +569,39 @@
},
/**
+ * @param {?Object} obj
+ */
+ _updateCachedAccount(obj) {
+ // If result of getAccount is in cache, update it in the cache
+ // so we don't have to invalidate it.
+ const cachedAccount = this._cache['/accounts/self/detail'];
+ if (cachedAccount) {
+ // Replace object in cache with new object to force UI updates.
+ this._cache['/accounts/self/detail'] =
+ Object.assign({}, cachedAccount, obj);
+ }
+ },
+
+ /**
* @param {string} name
* @param {function(?Response, string=)=} opt_errFn
* @param {?=} opt_ctx
*/
setAccountName(name, opt_errFn, opt_ctx) {
- return this.send('PUT', '/accounts/self/name', {name}, opt_errFn,
- opt_ctx).then(response => {
- // If result of getAccount is in cache, update it in the cache
- // so we don't have to invalidate it.
- const cachedAccount = this._cache['/accounts/self/detail'];
- if (cachedAccount) {
- return this.getResponseObject(response).then(newName => {
- // Replace object in cache with new object to force UI updates.
- // TODO(logan): Polyfill for Object.assign in IE
- this._cache['/accounts/self/detail'] = Object.assign(
- {}, cachedAccount, {name: newName});
- });
- }
- });
+ return this.send('PUT', '/accounts/self/name', {name}, opt_errFn, opt_ctx)
+ .then(response => this.getResponseObject(response)
+ .then(newName => this._updateCachedAccount({name: newName})));
+ },
+
+ /**
+ * @param {string} username
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {?=} opt_ctx
+ */
+ setAccountUsername(username, opt_errFn, opt_ctx) {
+ return this.send('PUT', '/accounts/self/username', {username}, opt_errFn,
+ opt_ctx).then(response => this.getResponseObject(response)
+ .then(newName => this._updateCachedAccount({username: newName})));
},
/**
@@ -581,19 +611,9 @@
*/
setAccountStatus(status, opt_errFn, opt_ctx) {
return this.send('PUT', '/accounts/self/status', {status},
- opt_errFn, opt_ctx).then(response => {
- // If result of getAccount is in cache, update it in the cache
- // so we don't have to invalidate it.
- const cachedAccount = this._cache['/accounts/self/detail'];
- if (cachedAccount) {
- return this.getResponseObject(response).then(newStatus => {
- // Replace object in cache with new object to force UI updates.
- // TODO(logan): Polyfill for Object.assign in IE
- this._cache['/accounts/self/detail'] = Object.assign(
- {}, cachedAccount, {status: newStatus});
- });
- }
- });
+ opt_errFn, opt_ctx).then(response => this.getResponseObject(response)
+ .then(newStatus => this._updateCachedAccount(
+ {status: newStatus})));
},
getAccountStatus(userId) {
@@ -825,6 +845,7 @@
this.ListChangesOption.CHANGE_ACTIONS,
this.ListChangesOption.CURRENT_ACTIONS,
this.ListChangesOption.DOWNLOAD_COMMANDS,
+ this.ListChangesOption.MESSAGES,
this.ListChangesOption.SUBMITTABLE,
this.ListChangesOption.WEB_LINKS
);
@@ -902,15 +923,18 @@
/**
* @param {number|string} changeNum
- * @param {!Promise<?Object>} patchRange
+ * @param {Defs.patchRange} patchRange
+ * @param {number=} opt_parentIndex
*/
- getChangeFiles(changeNum, patchRange) {
- let endpoint = '/files';
- if (patchRange.basePatchNum !== 'PARENT') {
- endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum);
+ getChangeFiles(changeNum, patchRange, opt_parentIndex) {
+ let params = undefined;
+ if (this.isMergeParent(patchRange.basePatchNum)) {
+ params = {parent: this.getParentIndex(patchRange.basePatchNum)};
+ } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
+ params = {base: patchRange.basePatchNum};
}
- return this._getChangeURLAndFetch(changeNum, endpoint,
- patchRange.patchNum);
+ return this._getChangeURLAndFetch(changeNum, '/files',
+ patchRange.patchNum, undefined, undefined, params);
},
/**
@@ -936,6 +960,11 @@
`/files?q=${encodeURIComponent(query)}`, patchNum);
},
+ /**
+ * @param {number|string} changeNum
+ * @param {Defs.patchRange} patchRange
+ * @return {!Promise<!Array<!Object>>}
+ */
getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) {
return this.getChangeFiles(changeNum, patchRange).then(
this._normalizeChangeFilesResponse.bind(this));
@@ -1416,7 +1445,8 @@
/**
* @param {number|string} changeNum
- * @param {number|string} basePatchNum
+ * @param {number|string} basePatchNum Negative values specify merge parent
+ * index.
* @param {number|string} patchNum
* @param {string} path
* @param {function(?Response, string=)=} opt_errFn
@@ -1429,7 +1459,9 @@
intraline: null,
whitespace: 'IGNORE_NONE',
};
- if (basePatchNum != PARENT_PATCH_NUM) {
+ if (this.isMergeParent(basePatchNum)) {
+ params.parent = this.getParentIndex(basePatchNum);
+ } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
params.base = basePatchNum;
}
const endpoint = `/files/${encodeURIComponent(path)}/diff`;
@@ -1610,6 +1642,7 @@
},
_sendDiffDraftRequest(method, changeNum, patchNum, draft) {
+ const isCreate = !draft.id && method === 'PUT';
let endpoint = '/drafts';
if (draft.id) {
endpoint += '/' + draft.id;
@@ -1626,6 +1659,11 @@
const promise = this.getChangeURLAndSend(changeNum, method, patchNum,
endpoint, body);
this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
+
+ if (isCreate) {
+ return this._failForCreate200(promise);
+ }
+
return promise;
},
@@ -1970,5 +2008,34 @@
`/files/${encodedPath}/blame`, patchNum, undefined, undefined,
opt_base ? {base: 't'} : undefined);
},
+
+ /**
+ * Modify the given create draft request promise so that it fails and throws
+ * an error if the response bears HTTP status 200 instead of HTTP 201.
+ * @see Issue 7763
+ * @param {Promise} promise The original promise.
+ * @return {Promise} The modified promise.
+ */
+ _failForCreate200(promise) {
+ return promise.then(result => {
+ if (result.status === 200) {
+ // Read the response headers into an object representation.
+ const headers = Array.from(result.headers.entries())
+ .reduce((obj, [key, val]) => {
+ if (!HEADER_REPORTING_BLACKLIST.test(key)) {
+ obj[key] = val;
+ }
+ return obj;
+ }, {});
+ const err = new Error([
+ CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
+ JSON.stringify(headers),
+ ].join('\n'));
+ // Throw the error so that it is caught by gr-reporting.
+ throw err;
+ }
+ return result;
+ });
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index f9604dc..fb5d475 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -651,24 +651,83 @@
});
});
- test('_sendDiffDraft pending requests tracked', () => {
- const obj = element._pendingRequests;
- sandbox.stub(element, 'getChangeURLAndSend', () => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
+ suite('draft comments', () => {
+ test('_sendDiffDraftRequest pending requests tracked', () => {
+ const obj = element._pendingRequests;
+ sandbox.stub(element, 'getChangeURLAndSend', () => mockPromise());
+ assert.notOk(element.hasPendingDiffDrafts());
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 1);
- assert.isTrue(!!element.hasPendingDiffDrafts());
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 1);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 2);
- assert.isTrue(!!element.hasPendingDiffDrafts());
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 2);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
- for (const promise of obj.sendDiffDraft) { promise.resolve(); }
+ for (const promise of obj.sendDiffDraft) { promise.resolve(); }
- return element.awaitPendingDiffDrafts().then(() => {
- assert.equal(obj.sendDiffDraft.length, 0);
- assert.isFalse(!!element.hasPendingDiffDrafts());
+ return element.awaitPendingDiffDrafts().then(() => {
+ assert.equal(obj.sendDiffDraft.length, 0);
+ assert.isFalse(!!element.hasPendingDiffDrafts());
+ });
+ });
+
+ suite('_failForCreate200', () => {
+ test('_sendDiffDraftRequest checks for 200 on create', () => {
+ const sendPromise = Promise.resolve();
+ sandbox.stub(element, 'getChangeURLAndSend').returns(sendPromise);
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
+ assert.isTrue(failStub.calledOnce);
+ assert.isTrue(failStub.calledWithExactly(sendPromise));
+ });
+ });
+
+ test('_sendDiffDraftRequest no checks for 200 on non create', () => {
+ sandbox.stub(element, 'getChangeURLAndSend')
+ .returns(Promise.resolve());
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
+ .then(() => {
+ assert.isFalse(failStub.called);
+ });
+ });
+
+ test('_failForCreate200 fails on 200', done => {
+ const result = {
+ ok: true,
+ status: 200,
+ headers: {entries: () => [
+ ['Set-CoOkiE', 'secret'],
+ ['Innocuous', 'hello'],
+ ]},
+ };
+ element._failForCreate200(Promise.resolve(result)).then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ }).catch(e => {
+ assert.isOk(e);
+ assert.include(e.message, 'Saving draft resulted in HTTP 200');
+ assert.include(e.message, 'hello');
+ assert.notInclude(e.message, 'secret');
+ done();
+ });
+ });
+
+ test('_failForCreate200 does not fail on 201', done => {
+ const result = {
+ ok: true,
+ status: 201,
+ headers: {entries: () => []},
+ };
+ element._failForCreate200(Promise.resolve(result)).then(() => {
+ done();
+ }).catch(e => {
+ assert.isTrue(false, 'Promise should not fail');
+ });
+ });
});
});
@@ -1036,5 +1095,82 @@
assert.deepEqual(sendSpy.lastCall.args[2], {generate: true});
});
});
+
+ suite('getChangeFiles', () => {
+ test('patch only', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: 'PARENT', patchNum: 2};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[2], 2);
+ assert.isNotOk(fetchStub.lastCall.args[5]);
+ });
+ });
+
+ test('simple range', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: 4, patchNum: 5};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[2], 5);
+ assert.isOk(fetchStub.lastCall.args[5]);
+ assert.equal(fetchStub.lastCall.args[5].base, 4);
+ assert.isNotOk(fetchStub.lastCall.args[5].parent);
+ });
+ });
+
+ test('parent index', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: -3, patchNum: 5};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[2], 5);
+ assert.isOk(fetchStub.lastCall.args[5]);
+ assert.isNotOk(fetchStub.lastCall.args[5].base);
+ assert.equal(fetchStub.lastCall.args[5].parent, 3);
+ });
+ });
+ });
+
+ suite('getDiff', () => {
+ test('patchOnly', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[2], 2);
+ assert.isOk(fetchStub.lastCall.args[5]);
+ assert.isNotOk(fetchStub.lastCall.args[5].parent);
+ assert.isNotOk(fetchStub.lastCall.args[5].base);
+ });
+ });
+
+ test('simple range', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[2], 5);
+ assert.isOk(fetchStub.lastCall.args[5]);
+ assert.isNotOk(fetchStub.lastCall.args[5].parent);
+ assert.equal(fetchStub.lastCall.args[5].base, 4);
+ });
+ });
+
+ test('parent index', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[2], 5);
+ assert.isOk(fetchStub.lastCall.args[5]);
+ assert.isNotOk(fetchStub.lastCall.args[5].base);
+ assert.equal(fetchStub.lastCall.args[5].parent, 3);
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index a8b8eef..21e5e1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -35,7 +35,14 @@
_updateValue() {
if (this.bindValue) {
+ // Set for chrome/safari so it happens instantly
this.nativeSelect.value = this.bindValue;
+ // Async needed for firefox to populate value. It was trying to do it
+ // before options from a dom-repeat were rendered previously.
+ // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735
+ this.async(() => {
+ this.nativeSelect.value = this.bindValue;
+ }, 1);
}
},
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 7f61edd..add690e 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -15,11 +15,16 @@
'use strict';
// Date cutoff is one day:
- const DRAFT_MAX_AGE = 24 * 60 * 60 * 1000;
+ const CLEANUP_MAX_AGE = 24 * 60 * 60 * 1000;
// Clean up old entries no more frequently than one day.
const CLEANUP_THROTTLE_INTERVAL = 24 * 60 * 60 * 1000;
+ const CLEANUP_PREFIXES = [
+ 'draft:',
+ 'editablecontent:',
+ ];
+
Polymer({
is: 'gr-storage',
@@ -39,7 +44,7 @@
},
getDraftComment(location) {
- this._cleanupDrafts();
+ this._cleanupItems();
return this._getObject(this._getDraftKey(location));
},
@@ -53,6 +58,16 @@
this._storage.removeItem(key);
},
+ setEditableContentItem(key, message) {
+ this._setObject(this._getEditableContentKey(key),
+ {message, updated: Date.now()});
+ },
+
+ getEditableContentItem(key) {
+ this._cleanupItems();
+ return this._getObject(this._getEditableContentKey(key));
+ },
+
getPreferences() {
return this._getObject('localPrefs');
},
@@ -74,7 +89,11 @@
return key;
},
- _cleanupDrafts() {
+ _getEditableContentKey(key) {
+ return `editablecontent:${key}`;
+ },
+
+ _cleanupItems() {
// Throttle cleanup to the throttle interval.
if (this._lastCleanup &&
Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
@@ -82,12 +101,16 @@
}
this._lastCleanup = Date.now();
- let draft;
+ let item;
for (const key in this._storage) {
- if (key.startsWith('draft:')) {
- draft = this._getObject(key);
- if (Date.now() - draft.updated > DRAFT_MAX_AGE) {
- this._storage.removeItem(key);
+ if (!this._storage.hasOwnProperty(key)) { continue; }
+ for (const prefix of CLEANUP_PREFIXES) {
+ if (key.startsWith(prefix)) {
+ item = this._getObject(key);
+ if (Date.now() - item.updated > CLEANUP_MAX_AGE) {
+ this._storage.removeItem(key);
+ }
+ break;
}
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index ce8ec20..b1a77c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -33,6 +33,7 @@
<script>
suite('gr-storage tests', () => {
let element;
+ let sandbox;
function mockStorage(opt_quotaExceeded) {
return {
@@ -48,9 +49,12 @@
setup(() => {
element = fixture('basic');
+ sandbox = sinon.sandbox.create();
element._storage = mockStorage();
});
+ teardown(() => sandbox.restore());
+
test('storing, retrieving and erasing drafts', () => {
const changeNum = 1234;
const patchNum = 5;
@@ -100,7 +104,7 @@
// Make sure that the call to cleanup doesn't get throttled.
element._lastCleanup = 0;
- const cleanupSpy = sinon.spy(element, '_cleanupDrafts');
+ const cleanupSpy = sandbox.spy(element, '_cleanupItems');
// Create a message with a timestamp that is a second behind the max age.
element._storage.setItem(key, JSON.stringify({
@@ -114,8 +118,6 @@
assert.isTrue(cleanupSpy.called);
assert.isNotOk(draft);
assert.isNotOk(element._storage.getItem(key));
-
- cleanupSpy.restore();
});
test('_getDraftKey', () => {
@@ -160,5 +162,28 @@
assert.isTrue(element._exceededQuota);
assert.isNotOk(element._storage.getItem(key));
});
+
+ test('editable content items', () => {
+ const cleanupStub = sandbox.stub(element, '_cleanupItems');
+ const key = 'testKey';
+ const computedKey = element._getEditableContentKey(key);
+ // Key correctly computed.
+ assert.equal(computedKey, 'editablecontent:testKey');
+
+ element.setEditableContentItem(key, 'my content');
+
+ // Setting the draft stores it under the expected key.
+ let item = element._storage.getItem(computedKey);
+ assert.isOk(item);
+ assert.equal(JSON.parse(item).message, 'my content');
+ assert.isOk(JSON.parse(item).updated);
+
+ // getEditableContentItem performs as expected.
+ item = element.getEditableContentItem(key);
+ assert.isOk(item);
+ assert.equal(item.message, 'my content');
+ assert.isOk(item.updated);
+ assert.isTrue(cleanupStub.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
index c34bc69..1744d28 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -24,7 +24,7 @@
--gr-tooltip-arrow-size: .5em;
--gr-tooltip-arrow-center-offset: 0;
- background-color: #333;
+ background-color: var(--tooltip-background-color, #333);
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
color: #fff;
font-size: .75rem;
@@ -52,11 +52,11 @@
width: 0;
}
.arrowPositionAbove {
- border-top: var(--gr-tooltip-arrow-size) solid #333;
+ border-top: var(--gr-tooltip-arrow-size) solid var(--tooltip-background-color, #333);
bottom: -var(--gr-tooltip-arrow-size);
}
.arrowPositionBelow {
- border-bottom: var(--gr-tooltip-arrow-size) solid #333;
+ border-bottom: var(--gr-tooltip-arrow-size) solid var(--tooltip-background-color, #333);
top: -var(--gr-tooltip-arrow-size);
}
</style>
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index 401f0aa..7c64db3 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -33,6 +33,7 @@
'GrRangeNormalizer',
'GrReporting',
'GrReviewerUpdatesParser',
+ 'GrCountStringFormatter',
'GrThemeApi',
'moment',
'page',
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 12e4b5f..a8ccbee 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -17,6 +17,7 @@
jar_filetype = FileType([".jar"])
LIBS = [
+ "//java/com/google/gerrit/common:version",
"//java/com/google/gerrit/httpd/init",
"//lib:postgresql",
"//lib/bouncycastle:bcpkix",
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
index 9eb6e34..9e8482e 100755
--- a/tools/js/npm_pack.py
+++ b/tools/js/npm_pack.py
@@ -13,6 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+"""This downloads an NPM binary, and bundles it with its dependencies.
+
+This is used to assemble a pinned version of crisper, hosted on the
+Google storage bucket ("repository=GERRIT" in WORKSPACE).
+"""
+
from __future__ import print_function
import atexit