Merge "Handle 0-length nodes while annotating diffs"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 452ef23..a9d922d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2766,6 +2766,22 @@
If not set or set to a zero, defaults to the number of logical CPUs as returned
by the JVM. If set to a negative value, defaults to a direct executor.
+[[index.change.indexMergeable]]index.change.indexMergeable::
++
+Specifies if `mergeable` should be index or not. Indexing this field enables
+queries that contain the mergeability operator (`is:mergeable`). If enabled,
+Gerrit will check if the change is mergeable into the target branch when
+reindexing a change. This is an expensive operation.
++
+If true, Gerrit will reindex all open changes when the target ref advances.
+Depending on the frequency of updates to the ref and the number of open changes,
+this can be very expensive.
++
+When this setting is changed from `false` to `true`, all changes need to be
+reindexed.
++
+Defaults to true.
+
[[index.onlineUpgrade]]index.onlineUpgrade::
+
Whether to upgrade to new index schema versions while the server is
@@ -2813,19 +2829,6 @@
+
Defaults to 1024.
-[[index.reindexAfterRefUpdate]]index.reindexAfterRefUpdate::
-+
-Whether to reindex all affected open changes after a ref is updated. This
-includes reindexing all open changes to recompute the "mergeable" bit every time
-the destination branch moves, as well as reindexing changes to take into account
-new project configuration (e.g. label definitions).
-+
-Leaving this enabled may result in fresher results, but may cause performance
-problems if there are lots of open changes on a project whose branches advance
-frequently.
-+
-Defaults to true.
-
[[index.autoReindexIfStale]]index.autoReindexIfStale::
+
Whether to automatically check if a document became stale in the index
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 18501b3..394886d 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -88,6 +88,8 @@
* openid:xerces
* polymer_externs:polymer_closure
* blame-cache
+* caffeine
+* caffeine-guava
* gson
* guava
* guava-failureaccess
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 69a7641..0fc733a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -315,6 +315,11 @@
the `mergeable` field will always be omitted and `SKIP_MERGEABLE` has no
effect.
+
+When link:config-gerrit.html#index.change.indexMergeable[
+`index.change.indexMergeable`] is set to `false` in the `gerrit.config`,
+the `mergeable` field will always be omitted when querying changes and
+`SKIP_MERGEABLE` has no effect.
++
A change's mergeability can be requested separately by calling the
link:#get-mergeable[get-mergeable] endpoint.
--
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 74ec8d3..c373b96 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3233,6 +3233,66 @@
HTTP/1.1 204 No Content
----
+[[batch-update-labels]]
+=== Batch Update Labels
+--
+'POST /projects/link:#project-name[\{project-name\}]/labels/'
+--
+
+Creates/updates/deletes multiple label definitions in this project at once.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+The updates must be specified in the request body as
+link:#batch-label-input[BatchLabelInput] entity.
+
+The updates are processed in the following order:
+
+1. label deletions
+2. label creations
+3. label updates
+
+.Request
+----
+ POST /projects/My-Project/labels/ HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "commit_message": "Update Labels",
+ "delete": [
+ "Old-Review",
+ "Unused-Review"
+ ],
+ "create": [
+ {
+ "name": "Foo-Review",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ }
+ ],
+ "update:" {
+ "Bar-Review": {
+ "function": "MaxWithBlock"
+ },
+ "Baz-Review": {
+ "copy_min_score": true
+ }
+ }
+ }
+----
+
+If the label updates were done successfully the response is "`200 OK`".
+
+.Response
+----
+ HTTP/1.1 200 OK
+----
+
[[ids]]
== IDs
@@ -3645,8 +3705,6 @@
|`inherited_value` |optional|
The inherited value of the configuration parameter, only set if
`inheritable` is true.
-|`permitted_values` |optional|
-The list of permitted values, only set if the `type` is `LIST`.
|===============================
[[dashboard-info]]
@@ -3876,9 +3934,14 @@
|Field Name ||Description
|`commit_message`|optional|
Message that should be used to commit the change of the label in the
-`project.config` file to the `refs/meta/config` branch.
+`project.config` file to the `refs/meta/config` branch.+
+Must not be set if this `LabelDefinitionInput` entity is contained in a
+link:#batch-label-input[BatchLabelInput] entity.
|`name` |optional|
-The new link:config-labels.html#label_name[name] of the label.
+The new link:config-labels.html#label_name[name] of the label.+
+For label creation the name is required if this `LabelDefinitionInput` entity
+is contained in a link:#batch-label-input[BatchLabelInput]
+entity.
|`function` |optional|
The new link:config-labels.html#label_function[function] of the label (can be
`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`.
@@ -3961,6 +4024,27 @@
Not set if not inherited or overridden.
|===============================
+[[batch-label-input]]
+=== BatchLabelInput
+The `BatchLabelInput` entity contains information for batch updating label
+definitions in a project.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`commit_message`|optional|
+Message that should be used to commit the label updates in the
+`project.config` file to the `refs/meta/config` branch.
+|`delete` |optional|
+List of labels that should be deleted.
+|`create` |optional|
+List of link:#label-definition-input[LabelDefinitionInput] entities that
+describe labels that should be created.
+|`update` |optional|
+Map of label names to link:#label-definition-input[LabelDefinitionInput]
+entities that describe the updates that should be done for the labels.
+|=============================
+
[[project-access-input]]
=== ProjectAccessInput
The `ProjectAccessInput` describes changes that should be applied to a project
diff --git a/WORKSPACE b/WORKSPACE
index 4e2c970..7529d8a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -241,6 +241,31 @@
sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
)
+CAFFEINE_VERS = "2.8.0"
+
+maven_jar(
+ name = "caffeine",
+ artifact = "com.github.ben-manes.caffeine:caffeine:" + CAFFEINE_VERS,
+ sha1 = "6000774d7f8412ced005a704188ced78beeed2bb",
+)
+
+# TODO(davido): Rename guava.jar to caffeine-guava.jar on fetch to prevent potential
+# naming collision between caffeine guava adapater and guava library itself.
+# Remove this renaming procedure, once this upstream issue is fixed:
+# https://github.com/ben-manes/caffeine/issues/364.
+http_file(
+ name = "caffeine-guava-renamed",
+ downloaded_file_path = "caffeine-guava-" + CAFFEINE_VERS + ".jar",
+ sha256 = "3a66ee3ec70971dee0bae6e56bda7b8742bc4bedd7489161bfbbaaf7137d89e1",
+ urls = [
+ "https://repo1.maven.org/maven2/com/github/ben-manes/caffeine/guava/" +
+ CAFFEINE_VERS +
+ "/guava-" +
+ CAFFEINE_VERS +
+ ".jar",
+ ],
+)
+
maven_jar(
name = "jsch",
artifact = "com.jcraft:jsch:0.1.54",
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 6e736a0..af8af90 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -563,8 +563,8 @@
cfg.setInt("sshd", null, "commandStartThreads", 1);
cfg.setInt("receive", null, "threadPoolSize", 1);
cfg.setInt("index", null, "threads", 1);
- if (cfg.getString("index", null, "reindexAfterRefUpdate") == null) {
- cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
+ if (cfg.getString("index", "change", "indexMergeable") == null) {
+ cfg.setBoolean("index", "change", "indexMergeable", false);
}
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index a06f90f..c3e3264 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -14,6 +14,7 @@
package com.google.gerrit.elasticsearch;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.elasticsearch.bulk.BulkRequest;
import com.google.gerrit.elasticsearch.bulk.IndexRequest;
@@ -74,7 +75,7 @@
public void replace(AccountState as) {
BulkRequest bulk =
new IndexRequest(getId(as), indexName, type, client.adapter())
- .add(new UpdateRequest<>(schema, as));
+ .add(new UpdateRequest<>(schema, as, ImmutableSet.of()));
String uri = getURI(type, BULK);
Response response = postRequest(uri, bulk, getRefreshParam());
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 37184cc..084c2ec 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -22,6 +22,7 @@
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
@@ -48,6 +49,7 @@
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.change.ChangeField;
@@ -65,6 +67,7 @@
import java.util.Optional;
import java.util.Set;
import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
import org.elasticsearch.client.Response;
/** Secondary index implementation using Elasticsearch. */
@@ -91,6 +94,7 @@
private final ChangeData.Factory changeDataFactory;
private final Schema<ChangeData> schema;
private final FieldDef<ChangeData, ?> idField;
+ private final ImmutableSet<String> skipFields;
@Inject
ElasticChangeIndex(
@@ -98,6 +102,7 @@
ChangeData.Factory changeDataFactory,
SitePaths sitePaths,
ElasticRestClientProvider clientBuilder,
+ @GerritServerConfig Config gerritConfig,
@Assisted Schema<ChangeData> schema) {
super(cfg, sitePaths, schema, clientBuilder, CHANGES);
this.changeDataFactory = changeDataFactory;
@@ -105,6 +110,10 @@
this.mapping = new ChangeMapping(schema, client.adapter());
this.idField =
this.schema.useLegacyNumericFields() ? ChangeField.LEGACY_ID : ChangeField.LEGACY_ID_STR;
+ this.skipFields =
+ gerritConfig.getBoolean("index", "change", "indexMergeable", true)
+ ? ImmutableSet.of()
+ : ImmutableSet.of(ChangeField.MERGEABLE.getName());
}
@Override
@@ -123,7 +132,7 @@
ElasticQueryAdapter adapter = client.adapter();
BulkRequest bulk =
new IndexRequest(getId(cd), indexName, adapter.getType(insertIndex), adapter)
- .add(new UpdateRequest<>(schema, cd));
+ .add(new UpdateRequest<>(schema, cd, skipFields));
if (adapter.deleteToReplace()) {
bulk.add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex, adapter));
}
@@ -263,7 +272,7 @@
// Mergeable.
JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName());
- if (mergeableElement != null) {
+ if (mergeableElement != null && !skipFields.contains(ChangeField.MERGEABLE.getName())) {
String mergeable = mergeableElement.getAsString();
if ("1".equals(mergeable)) {
cd.setMergeable(true);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index c215132..ce2025f 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -14,6 +14,7 @@
package com.google.gerrit.elasticsearch;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.elasticsearch.bulk.BulkRequest;
import com.google.gerrit.elasticsearch.bulk.IndexRequest;
@@ -74,7 +75,7 @@
public void replace(InternalGroup group) {
BulkRequest bulk =
new IndexRequest(getId(group), indexName, type, client.adapter())
- .add(new UpdateRequest<>(schema, group));
+ .add(new UpdateRequest<>(schema, group, ImmutableSet.of()));
String uri = getURI(type, BULK);
Response response = postRequest(uri, bulk, getRefreshParam());
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index 29f8507..b636706 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -14,6 +14,7 @@
package com.google.gerrit.elasticsearch;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.elasticsearch.bulk.BulkRequest;
import com.google.gerrit.elasticsearch.bulk.IndexRequest;
@@ -74,7 +75,7 @@
public void replace(ProjectData projectState) {
BulkRequest bulk =
new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter())
- .add(new UpdateRequest<>(schema, projectState));
+ .add(new UpdateRequest<>(schema, projectState, ImmutableSet.of()));
String uri = getURI(type, BULK);
Response response = postRequest(uri, bulk, getRefreshParam());
diff --git a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
index 2f0bd01..196b8d6 100644
--- a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
+++ b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
@@ -16,6 +16,7 @@
import static java.util.stream.Collectors.toList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.gerrit.elasticsearch.builders.XContentBuilder;
@@ -27,17 +28,19 @@
private final Schema<V> schema;
private final V v;
+ private final ImmutableSet<String> skipFields;
- public UpdateRequest(Schema<V> schema, V v) {
+ public UpdateRequest(Schema<V> schema, V v, ImmutableSet<String> skipFields) {
this.schema = schema;
this.v = v;
+ this.skipFields = skipFields;
}
@Override
protected String getRequest() {
try (XContentBuilder closeable = new XContentBuilder()) {
XContentBuilder builder = closeable.startObject();
- for (Values<V> values : schema.buildFields(v)) {
+ for (Values<V> values : schema.buildFields(v, skipFields)) {
String name = values.getField().getName();
if (values.getField().isRepeatable()) {
builder.field(name, Streams.stream(values.getValues()).collect(toList()));
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 6d02ec4..9873995 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.config.AccessCheckInfo;
import com.google.gerrit.extensions.api.config.AccessCheckInput;
+import com.google.gerrit.extensions.common.BatchLabelInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.common.ProjectInfo;
@@ -221,6 +222,13 @@
LabelApi label(String labelName) throws RestApiException;
/**
+ * Adds, updates and deletes label definitions in a batch.
+ *
+ * @param input input that describes additions, updates and deletions of label definitions
+ */
+ void labels(BatchLabelInput input) throws RestApiException;
+
+ /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
@@ -404,5 +412,10 @@
public LabelApi label(String labelName) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public void labels(BatchLabelInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/common/BatchLabelInput.java b/java/com/google/gerrit/extensions/common/BatchLabelInput.java
new file mode 100644
index 0000000..eb4c581
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/BatchLabelInput.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2019 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.extensions.common;
+
+import java.util.List;
+import java.util.Map;
+
+/** Input for the REST API that describes additions, updates and deletions of label definitions. */
+public class BatchLabelInput {
+ public String commitMessage;
+ public List<String> delete;
+ public List<LabelDefinitionInput> create;
+ public Map<String, LabelDefinitionInput> update;
+}
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index f9f8c48..0aa374b 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -15,11 +15,12 @@
package com.google.gerrit.index;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.MoreObjects;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import java.util.ArrayList;
import java.util.Arrays;
@@ -181,12 +182,17 @@
* <p>Null values are omitted, as are fields which cause errors, which are logged.
*
* @param obj input object.
+ * @param skipFields set of field names to skip when indexing the document
* @return all non-null field values from the object.
*/
- public final Iterable<Values<T>> buildFields(T obj) {
- return FluentIterable.from(fields.values())
- .transform(
+ public final Iterable<Values<T>> buildFields(T obj, ImmutableSet<String> skipFields) {
+ return fields.values().stream()
+ .map(
f -> {
+ if (skipFields.contains(f.getName())) {
+ return null;
+ }
+
Object v;
try {
v = f.get(obj);
@@ -203,7 +209,8 @@
return new Values<>(f, Collections.singleton(v));
}
})
- .filter(Objects::nonNull);
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
}
@Override
diff --git a/java/com/google/gerrit/index/query/RangeUtil.java b/java/com/google/gerrit/index/query/RangeUtil.java
index 1f22f36..cfe1929 100644
--- a/java/com/google/gerrit/index/query/RangeUtil.java
+++ b/java/com/google/gerrit/index/query/RangeUtil.java
@@ -106,6 +106,10 @@
break;
}
+ // Ensure that minValue <= min/max <= maxValue.
+ min = Ints.constrainToRange(min, minValue, maxValue);
+ max = Ints.constrainToRange(max, minValue, maxValue);
+
return new Range(prefix, min, max);
}
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index deb3203..5392ab4 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -21,6 +21,7 @@
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
@@ -98,6 +99,7 @@
private final SitePaths sitePaths;
private final Directory dir;
private final String name;
+ private final ImmutableSet<String> skipFields;
private final ListeningExecutorService writerThread;
private final IndexWriter writer;
private final ReferenceManager<IndexSearcher> searcherManager;
@@ -110,6 +112,7 @@
SitePaths sitePaths,
Directory dir,
String name,
+ ImmutableSet<String> skipFields,
String subIndex,
GerritIndexWriterConfig writerConfig,
SearcherFactory searcherFactory)
@@ -118,6 +121,7 @@
this.sitePaths = sitePaths;
this.dir = dir;
this.name = name;
+ this.skipFields = skipFields;
String index = Joiner.on('_').skipNulls().join(name, subIndex);
long commitPeriod = writerConfig.getCommitWithinMs();
@@ -311,7 +315,7 @@
Document toDocument(V obj) {
Document result = new Document();
- for (Values<V> vs : schema.buildFields(obj)) {
+ for (Values<V> vs : schema.buildFields(obj, skipFields)) {
if (vs.getValues() != null) {
add(result, vs);
}
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index fd439f1..e51a91a7 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.lucene.LuceneChangeIndex.UPDATED_SORT_FIELD;
import static com.google.gerrit.server.index.change.ChangeSchemaDefinitions.NAME;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Change;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.QueryOptions;
@@ -48,6 +49,7 @@
Schema<ChangeData> schema,
SitePaths sitePaths,
Path path,
+ ImmutableSet<String> skipFields,
GerritIndexWriterConfig writerConfig,
SearcherFactory searcherFactory)
throws IOException {
@@ -56,6 +58,7 @@
sitePaths,
FSDirectory.open(path),
path.getFileName().toString(),
+ skipFields,
writerConfig,
searcherFactory);
}
@@ -65,10 +68,11 @@
SitePaths sitePaths,
Directory dir,
String subIndex,
+ ImmutableSet<String> skipFields,
GerritIndexWriterConfig writerConfig,
SearcherFactory searcherFactory)
throws IOException {
- super(schema, sitePaths, dir, NAME, subIndex, writerConfig, searcherFactory);
+ super(schema, sitePaths, dir, NAME, skipFields, subIndex, writerConfig, searcherFactory);
}
@Override
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index efd7ea3..242cffd 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.server.index.account.AccountField.ID_STR;
import static com.google.gerrit.server.index.account.AccountField.PREFERRED_EMAIL_EXACT;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.FieldDef;
@@ -100,6 +101,7 @@
sitePaths,
dir(schema, cfg, sitePaths),
ACCOUNTS,
+ ImmutableSet.of(),
null,
new GerritIndexWriterConfig(cfg, ACCOUNTS),
new SearcherFactory());
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 16d66b6..3b277dd 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -29,6 +29,7 @@
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
@@ -167,6 +168,7 @@
private final String idSortFieldName;
private final IdTerm idTerm;
private final ChangeIdExtractor extractor;
+ private final ImmutableSet<String> skipFields;
@Inject
LuceneChangeIndex(
@@ -179,6 +181,10 @@
this.executor = executor;
this.changeDataFactory = changeDataFactory;
this.schema = schema;
+ this.skipFields =
+ cfg.getBoolean("index", "change", "indexMergeable", true)
+ ? ImmutableSet.of()
+ : ImmutableSet.of(ChangeField.MERGEABLE.getName());
GerritIndexWriterConfig openConfig = new GerritIndexWriterConfig(cfg, "changes_open");
GerritIndexWriterConfig closedConfig = new GerritIndexWriterConfig(cfg, "changes_closed");
@@ -189,18 +195,40 @@
if (LuceneIndexModule.isInMemoryTest(cfg)) {
openIndex =
new ChangeSubIndex(
- schema, sitePaths, new RAMDirectory(), "ramOpen", openConfig, searcherFactory);
+ schema,
+ sitePaths,
+ new RAMDirectory(),
+ "ramOpen",
+ skipFields,
+ openConfig,
+ searcherFactory);
closedIndex =
new ChangeSubIndex(
- schema, sitePaths, new RAMDirectory(), "ramClosed", closedConfig, searcherFactory);
+ schema,
+ sitePaths,
+ new RAMDirectory(),
+ "ramClosed",
+ skipFields,
+ closedConfig,
+ searcherFactory);
} else {
Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES, schema);
openIndex =
new ChangeSubIndex(
- schema, sitePaths, dir.resolve(CHANGES_OPEN), openConfig, searcherFactory);
+ schema,
+ sitePaths,
+ dir.resolve(CHANGES_OPEN),
+ skipFields,
+ openConfig,
+ searcherFactory);
closedIndex =
new ChangeSubIndex(
- schema, sitePaths, dir.resolve(CHANGES_CLOSED), closedConfig, searcherFactory);
+ schema,
+ sitePaths,
+ dir.resolve(CHANGES_CLOSED),
+ skipFields,
+ closedConfig,
+ searcherFactory);
}
idField = this.schema.useLegacyNumericFields() ? LEGACY_ID : LEGACY_ID_STR;
@@ -565,7 +593,7 @@
private void decodeMergeable(ListMultimap<String, IndexableField> doc, ChangeData cd) {
IndexableField f = Iterables.getFirst(doc.get(MERGEABLE_FIELD), null);
- if (f != null) {
+ if (f != null && !skipFields.contains(MERGEABLE_FIELD)) {
String mergeable = f.stringValue();
if ("1".equals(mergeable)) {
cd.setMergeable(true);
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index 99cd40d..3d1d471 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -17,6 +17,7 @@
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.gerrit.server.index.group.GroupField.UUID;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.FieldDef;
@@ -90,6 +91,7 @@
sitePaths,
dir(schema, cfg, sitePaths),
GROUPS,
+ ImmutableSet.of(),
null,
new GerritIndexWriterConfig(cfg, GROUPS),
new SearcherFactory());
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 97454c7..a3a0d9c 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -17,6 +17,7 @@
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.gerrit.index.project.ProjectField.NAME;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.FieldDef;
@@ -90,6 +91,7 @@
sitePaths,
dir(schema, cfg, sitePaths),
PROJECTS,
+ ImmutableSet.of(),
null,
new GerritIndexWriterConfig(cfg, PROJECTS),
new SearcherFactory());
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index d7ab91b..6d7fc15 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -43,6 +43,7 @@
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.api.projects.TagApi;
import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.BatchLabelInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
@@ -77,6 +78,7 @@
import com.google.gerrit.server.restapi.project.ListDashboards;
import com.google.gerrit.server.restapi.project.ListLabels;
import com.google.gerrit.server.restapi.project.ListTags;
+import com.google.gerrit.server.restapi.project.PostLabels;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
import com.google.gerrit.server.restapi.project.PutConfig;
import com.google.gerrit.server.restapi.project.PutDescription;
@@ -131,6 +133,7 @@
private final Index index;
private final IndexChanges indexChanges;
private final Provider<ListLabels> listLabels;
+ private final PostLabels postLabels;
private final LabelApiImpl.Factory labelApi;
@AssistedInject
@@ -168,6 +171,7 @@
Index index,
IndexChanges indexChanges,
Provider<ListLabels> listLabels,
+ PostLabels postLabels,
LabelApiImpl.Factory labelApi,
@Assisted ProjectResource project) {
this(
@@ -205,6 +209,7 @@
index,
indexChanges,
listLabels,
+ postLabels,
labelApi,
null);
}
@@ -244,6 +249,7 @@
Index index,
IndexChanges indexChanges,
Provider<ListLabels> listLabels,
+ PostLabels postLabels,
LabelApiImpl.Factory labelApi,
@Assisted String name) {
this(
@@ -281,6 +287,7 @@
index,
indexChanges,
listLabels,
+ postLabels,
labelApi,
name);
}
@@ -320,6 +327,7 @@
Index index,
IndexChanges indexChanges,
Provider<ListLabels> listLabels,
+ PostLabels postLabels,
LabelApiImpl.Factory labelApi,
String name) {
this.permissionBackend = permissionBackend;
@@ -357,6 +365,7 @@
this.index = index;
this.indexChanges = indexChanges;
this.listLabels = listLabels;
+ this.postLabels = postLabels;
this.labelApi = labelApi;
}
@@ -712,4 +721,13 @@
throw asRestApiException("Cannot parse label", e);
}
}
+
+ @Override
+ public void labels(BatchLabelInput input) throws RestApiException {
+ try {
+ postLabels.apply(checkExists(), input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot update labels", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/cache/CacheBackend.java b/java/com/google/gerrit/server/cache/CacheBackend.java
new file mode 100644
index 0000000..ec9876f
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/CacheBackend.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2019 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.cache;
+
+/** Caffeine is used as default cache backend, but can be overridden with Guava backend. */
+public enum CacheBackend {
+ CAFFEINE,
+ GUAVA;
+
+ public boolean isLegacyBackend() {
+ return this == GUAVA;
+ }
+}
diff --git a/java/com/google/gerrit/server/cache/CacheModule.java b/java/com/google/gerrit/server/cache/CacheModule.java
index 2878624..0fdc6f5 100644
--- a/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/java/com/google/gerrit/server/cache/CacheModule.java
@@ -68,7 +68,8 @@
*/
protected <K, V> CacheBinding<K, V> cache(
String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
- CacheProvider<K, V> m = new CacheProvider<>(this, name, keyType, valType);
+ CacheProvider<K, V> m =
+ new CacheProvider<>(this, name, keyType, valType, CacheBackend.CAFFEINE);
bindCache(m, name, keyType, valType);
return m;
}
@@ -123,7 +124,20 @@
*/
protected <K, V> PersistentCacheBinding<K, V> persist(
String name, Class<K> keyType, Class<V> valType) {
- return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
+ return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType), CacheBackend.CAFFEINE);
+ }
+
+ /**
+ * Declare a named in-memory/on-disk cache.
+ *
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @param backend cache backend.
+ * @return binding to describe the cache.
+ */
+ protected <K, V> PersistentCacheBinding<K, V> persist(
+ String name, Class<K> keyType, Class<V> valType, CacheBackend backend) {
+ return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType), backend);
}
/**
@@ -135,7 +149,7 @@
*/
protected <K, V> PersistentCacheBinding<K, V> persist(
String name, Class<K> keyType, TypeLiteral<V> valType) {
- return persist(name, TypeLiteral.get(keyType), valType);
+ return persist(name, TypeLiteral.get(keyType), valType, CacheBackend.CAFFEINE);
}
/**
@@ -146,8 +160,9 @@
* @return binding to describe the cache.
*/
protected <K, V> PersistentCacheBinding<K, V> persist(
- String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
- PersistentCacheProvider<K, V> m = new PersistentCacheProvider<>(this, name, keyType, valType);
+ String name, TypeLiteral<K> keyType, TypeLiteral<V> valType, CacheBackend backend) {
+ PersistentCacheProvider<K, V> m =
+ new PersistentCacheProvider<>(this, name, keyType, valType, backend);
bindCache(m, name, keyType, valType);
Type cacheDefType =
diff --git a/java/com/google/gerrit/server/cache/CacheProvider.java b/java/com/google/gerrit/server/cache/CacheProvider.java
index b1a9b91..fe4244c 100644
--- a/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -30,6 +30,7 @@
class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>, CacheDef<K, V> {
private final CacheModule module;
+ private final CacheBackend backend;
final String name;
private final TypeLiteral<K> keyType;
private final TypeLiteral<V> valType;
@@ -44,11 +45,17 @@
private MemoryCacheFactory memoryCacheFactory;
private boolean frozen;
- CacheProvider(CacheModule module, String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
+ CacheProvider(
+ CacheModule module,
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType,
+ CacheBackend backend) {
this.module = module;
this.name = name;
this.keyType = keyType;
this.valType = valType;
+ this.backend = backend;
}
@Inject(optional = true)
@@ -159,7 +166,9 @@
public Cache<K, V> get() {
freeze();
CacheLoader<K, V> ldr = loader();
- return ldr != null ? memoryCacheFactory.build(this, ldr) : memoryCacheFactory.build(this);
+ return ldr != null
+ ? memoryCacheFactory.build(this, ldr, backend)
+ : memoryCacheFactory.build(this, backend);
}
protected void checkNotFrozen() {
diff --git a/java/com/google/gerrit/server/cache/MemoryCacheFactory.java b/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
index fc55753..558380d 100644
--- a/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
@@ -19,7 +19,8 @@
import com.google.common.cache.LoadingCache;
public interface MemoryCacheFactory {
- <K, V> Cache<K, V> build(CacheDef<K, V> def);
+ <K, V> Cache<K, V> build(CacheDef<K, V> def, CacheBackend backend);
- <K, V> LoadingCache<K, V> build(CacheDef<K, V> def, CacheLoader<K, V> loader);
+ <K, V> LoadingCache<K, V> build(
+ CacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend);
}
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheFactory.java b/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index 27fa9ca..93f91ef 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
@@ -19,9 +19,10 @@
import com.google.common.cache.LoadingCache;
public interface PersistentCacheFactory {
- <K, V> Cache<K, V> build(PersistentCacheDef<K, V> def);
+ <K, V> Cache<K, V> build(PersistentCacheDef<K, V> def, CacheBackend backend);
- <K, V> LoadingCache<K, V> build(PersistentCacheDef<K, V> def, CacheLoader<K, V> loader);
+ <K, V> LoadingCache<K, V> build(
+ PersistentCacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend);
void onStop(String plugin);
}
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
index 59d66e3..4fc107f 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
@@ -30,6 +30,7 @@
class PersistentCacheProvider<K, V> extends CacheProvider<K, V>
implements Provider<Cache<K, V>>, PersistentCacheBinding<K, V>, PersistentCacheDef<K, V> {
+ private final CacheBackend backend;
private int version;
private long diskLimit;
private CacheSerializer<K> keySerializer;
@@ -39,9 +40,19 @@
PersistentCacheProvider(
CacheModule module, String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
- super(module, name, keyType, valType);
+ this(module, name, keyType, valType, CacheBackend.CAFFEINE);
+ }
+
+ PersistentCacheProvider(
+ CacheModule module,
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType,
+ CacheBackend backend) {
+ super(module, name, keyType, valType, backend);
version = -1;
diskLimit = 128 << 20;
+ this.backend = backend;
}
@Inject(optional = true)
@@ -130,8 +141,8 @@
freeze();
CacheLoader<K, V> ldr = loader();
return ldr != null
- ? persistentCacheFactory.build(this, ldr)
- : persistentCacheFactory.build(this);
+ ? persistentCacheFactory.build(this, ldr, backend)
+ : persistentCacheFactory.build(this, backend);
}
private static <T> void checkSerializer(
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index af1228d..2b068aa 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -21,6 +21,7 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.MemoryCacheFactory;
import com.google.gerrit.server.cache.PersistentCacheDef;
import com.google.gerrit.server.cache.PersistentCacheFactory;
@@ -156,18 +157,21 @@
@SuppressWarnings({"unchecked"})
@Override
- public <K, V> Cache<K, V> build(PersistentCacheDef<K, V> in) {
+ public <K, V> Cache<K, V> build(PersistentCacheDef<K, V> in, CacheBackend backend) {
long limit = config.getLong("cache", in.configKey(), "diskLimit", in.diskLimit());
if (cacheDir == null || limit <= 0) {
- return memCacheFactory.build(in);
+ return memCacheFactory.build(in, backend);
}
H2CacheDefProxy<K, V> def = new H2CacheDefProxy<>(in);
SqlStore<K, V> store = newSqlStore(def, limit);
H2CacheImpl<K, V> cache =
new H2CacheImpl<>(
- executor, store, def.keyType(), (Cache<K, ValueHolder<V>>) memCacheFactory.build(def));
+ executor,
+ store,
+ def.keyType(),
+ (Cache<K, ValueHolder<V>>) memCacheFactory.build(def, backend));
synchronized (caches) {
caches.add(cache);
}
@@ -176,11 +180,12 @@
@SuppressWarnings("unchecked")
@Override
- public <K, V> LoadingCache<K, V> build(PersistentCacheDef<K, V> in, CacheLoader<K, V> loader) {
+ public <K, V> LoadingCache<K, V> build(
+ PersistentCacheDef<K, V> in, CacheLoader<K, V> loader, CacheBackend backend) {
long limit = config.getLong("cache", in.configKey(), "diskLimit", in.diskLimit());
if (cacheDir == null || limit <= 0) {
- return memCacheFactory.build(in, loader);
+ return memCacheFactory.build(in, loader, backend);
}
H2CacheDefProxy<K, V> def = new H2CacheDefProxy<>(in);
@@ -188,7 +193,9 @@
Cache<K, ValueHolder<V>> mem =
(Cache<K, ValueHolder<V>>)
memCacheFactory.build(
- def, (CacheLoader<K, V>) new H2CacheImpl.Loader<>(executor, store, loader));
+ def,
+ (CacheLoader<K, V>) new H2CacheImpl.Loader<>(executor, store, loader),
+ backend);
H2CacheImpl<K, V> cache = new H2CacheImpl<>(executor, store, def.keyType(), mem);
synchronized (caches) {
caches.add(cache);
diff --git a/java/com/google/gerrit/server/cache/mem/BUILD b/java/com/google/gerrit/server/cache/mem/BUILD
index d805e1f..a666df7 100644
--- a/java/com/google/gerrit/server/cache/mem/BUILD
+++ b/java/com/google/gerrit/server/cache/mem/BUILD
@@ -8,6 +8,8 @@
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/server",
+ "//lib:caffeine",
+ "//lib:caffeine-guava",
"//lib:guava",
"//lib:jgit",
"//lib/guice",
diff --git a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index f76b8db..9906b3d 100644
--- a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -17,13 +17,18 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.RemovalListener;
+import com.github.benmanes.caffeine.cache.Weigher;
+import com.github.benmanes.caffeine.guava.CaffeinatedGuava;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
-import com.google.common.cache.Weigher;
+import com.google.common.cache.RemovalNotification;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.CacheDef;
import com.google.gerrit.server.cache.ForwardingRemovalListener;
import com.google.gerrit.server.cache.MemoryCacheFactory;
@@ -46,25 +51,30 @@
}
@Override
- public <K, V> Cache<K, V> build(CacheDef<K, V> def) {
- return create(def).build();
+ public <K, V> Cache<K, V> build(CacheDef<K, V> def, CacheBackend backend) {
+ return backend.isLegacyBackend()
+ ? createLegacy(def).build()
+ : CaffeinatedGuava.build(create(def));
}
@Override
- public <K, V> LoadingCache<K, V> build(CacheDef<K, V> def, CacheLoader<K, V> loader) {
- return create(def).build(loader);
+ public <K, V> LoadingCache<K, V> build(
+ CacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend) {
+ return backend.isLegacyBackend()
+ ? createLegacy(def).build(loader)
+ : CaffeinatedGuava.build(create(def), loader);
}
@SuppressWarnings("unchecked")
- private <K, V> CacheBuilder<K, V> create(CacheDef<K, V> def) {
- CacheBuilder<K, V> builder = newCacheBuilder();
+ private <K, V> CacheBuilder<K, V> createLegacy(CacheDef<K, V> def) {
+ CacheBuilder<K, V> builder = newLegacyCacheBuilder();
builder.recordStats();
builder.maximumWeight(
cfg.getLong("cache", def.configKey(), "memoryLimit", def.maximumWeight()));
builder = builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
- Weigher<K, V> weigher = def.weigher();
+ com.google.common.cache.Weigher<K, V> weigher = def.weigher();
if (weigher == null) {
weigher = unitWeight();
}
@@ -98,6 +108,42 @@
return builder;
}
+ private <K, V> Caffeine<K, V> create(CacheDef<K, V> def) {
+ Caffeine<K, V> builder = newCacheBuilder();
+ builder.recordStats();
+ builder.maximumWeight(
+ cfg.getLong("cache", def.configKey(), "memoryLimit", def.maximumWeight()));
+ builder = builder.removalListener(newRemovalListener(def.name()));
+ builder.weigher(newWeigher(def.weigher()));
+
+ Duration expireAfterWrite = def.expireAfterWrite();
+ if (has(def.configKey(), "maxAge")) {
+ builder.expireAfterWrite(
+ ConfigUtil.getTimeUnit(
+ cfg, "cache", def.configKey(), "maxAge", toSeconds(expireAfterWrite), SECONDS),
+ SECONDS);
+ } else if (expireAfterWrite != null) {
+ builder.expireAfterWrite(expireAfterWrite.toNanos(), NANOSECONDS);
+ }
+
+ Duration expireAfterAccess = def.expireFromMemoryAfterAccess();
+ if (has(def.configKey(), "expireFromMemoryAfterAccess")) {
+ builder.expireAfterAccess(
+ ConfigUtil.getTimeUnit(
+ cfg,
+ "cache",
+ def.configKey(),
+ "expireFromMemoryAfterAccess",
+ toSeconds(expireAfterAccess),
+ SECONDS),
+ SECONDS);
+ } else if (expireAfterAccess != null) {
+ builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
+ }
+
+ return builder;
+ }
+
private static long toSeconds(@Nullable Duration duration) {
return duration != null ? duration.getSeconds() : 0;
}
@@ -107,11 +153,31 @@
}
@SuppressWarnings("unchecked")
- private static <K, V> CacheBuilder<K, V> newCacheBuilder() {
+ private static <K, V> CacheBuilder<K, V> newLegacyCacheBuilder() {
return (CacheBuilder<K, V>) CacheBuilder.newBuilder();
}
- private static <K, V> Weigher<K, V> unitWeight() {
+ private static <K, V> com.google.common.cache.Weigher<K, V> unitWeight() {
return (key, value) -> 1;
}
+
+ @SuppressWarnings("unchecked")
+ private static <K, V> Caffeine<K, V> newCacheBuilder() {
+ return (Caffeine<K, V>) Caffeine.newBuilder();
+ }
+
+ @SuppressWarnings("unchecked")
+ private <V, K> RemovalListener<K, V> newRemovalListener(String cacheName) {
+ return (k, v, cause) ->
+ forwardingRemovalListenerFactory
+ .create(cacheName)
+ .onRemoval(
+ RemovalNotification.create(
+ k, v, com.google.common.cache.RemovalCause.valueOf(cause.name())));
+ }
+
+ private static <K, V> Weigher<K, V> newWeigher(
+ com.google.common.cache.Weigher<K, V> guavaWeigher) {
+ return guavaWeigher == null ? Weigher.singletonWeigher() : (k, v) -> guavaWeigher.weigh(k, v);
+ }
}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 9937fd0..21ee28a 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -138,6 +138,7 @@
COMMIT_FOOTERS,
CURRENT_ACTIONS,
CURRENT_COMMIT,
+ DETAILED_LABELS, // may need to load ChangeNotes to check remove reviewer permissions
MESSAGES);
@Singleton
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 7038736..7767fe2 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -310,7 +310,8 @@
allRefsWatcher = new AllRefsWatcher();
receivePack.setAdvertiseRefsHook(
- ReceiveCommitsAdvertiseRefsHookChain.create(allRefsWatcher, queryProvider, projectName));
+ ReceiveCommitsAdvertiseRefsHookChain.create(
+ allRefsWatcher, queryProvider, projectName, user.getAccountId()));
resultChangeIds = new ResultChangeIds();
receiveCommits =
factory.create(
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 2b04d4d..7402a37 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -15,6 +15,7 @@
"//java/com/google/gerrit/exceptions",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/git",
+ "//java/com/google/gerrit/index",
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/logging",
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 83bf554..6c1f097 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -18,14 +18,19 @@
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.git.HookUtil;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.query.change.OwnerPredicate;
+import com.google.gerrit.server.query.change.ProjectPredicate;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Provider;
import java.io.IOException;
@@ -65,11 +70,13 @@
private final Provider<InternalChangeQuery> queryProvider;
private final Project.NameKey projectName;
+ private final Account.Id user;
public ReceiveCommitsAdvertiseRefsHook(
- Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName) {
+ Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, Account.Id user) {
this.queryProvider = queryProvider;
this.projectName = projectName;
+ this.user = user;
}
@Override
@@ -90,7 +97,9 @@
private Set<ObjectId> advertiseOpenChanges(Repository repo)
throws ServiceMayNotContinueException {
- // Advertise some recent open changes, in case a commit is based on one.
+ // Advertise the user's most recent open changes. It's likely that the user has one of these in
+ // their local repo and they can serve as starting points to figure out the common ancestor of
+ // what the client and server have in common.
int limit = 32;
try {
Set<ObjectId> r = Sets.newHashSetWithExpectedSize(limit);
@@ -105,7 +114,11 @@
ChangeField.PATCH_SET)
.enforceVisibility(true)
.setLimit(limit)
- .byProjectOpen(projectName)) {
+ .query(
+ Predicate.and(
+ new ProjectPredicate(projectName.get()),
+ ChangeStatusPredicate.open(),
+ new OwnerPredicate(user)))) {
PatchSet ps = cd.currentPatchSet();
if (ps != null) {
// Ensure we actually observed a patch set ref pointing to this
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java
index 76f6b04..fae1401 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHookChain.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git.receive;
import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Provider;
@@ -35,8 +36,9 @@
public static AdvertiseRefsHook create(
AllRefsWatcher allRefsWatcher,
Provider<InternalChangeQuery> queryProvider,
- Project.NameKey projectName) {
- return create(allRefsWatcher, queryProvider, projectName, false);
+ Project.NameKey projectName,
+ Account.Id user) {
+ return create(allRefsWatcher, queryProvider, projectName, user, false);
}
/**
@@ -47,18 +49,19 @@
*/
@VisibleForTesting
public static AdvertiseRefsHook createForTest(
- Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName) {
- return create(new AllRefsWatcher(), queryProvider, projectName, true);
+ Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, Account.Id user) {
+ return create(new AllRefsWatcher(), queryProvider, projectName, user, true);
}
private static AdvertiseRefsHook create(
AllRefsWatcher allRefsWatcher,
Provider<InternalChangeQuery> queryProvider,
Project.NameKey projectName,
+ Account.Id user,
boolean skipHackPushNegotiateHook) {
List<AdvertiseRefsHook> advHooks = new ArrayList<>();
advHooks.add(allRefsWatcher);
- advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
+ advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName, user));
if (!skipHackPushNegotiateHook) {
advHooks.add(new HackPushNegotiateHook());
}
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index f6d3b6f..5efa065 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -76,7 +76,7 @@
this.accountCache = accountCache;
this.indexer = indexer;
this.executor = executor;
- this.enabled = cfg.getBoolean("index", null, "reindexAfterRefUpdate", true);
+ this.enabled = cfg.getBoolean("index", "change", "indexMergeable", true);
}
@Override
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 1c0f298..7ca0b86 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -233,20 +233,13 @@
return NO_OP_UPDATE;
}
- // If we touched every revision and there are no comments left, tell the
+ // If there are no comments left, tell the
// caller to delete the entire ref.
- boolean touchedAllRevs = updatedCommits.equals(rnm.revisionNotes.keySet());
- if (touchedAllRevs && !hasComments) {
+ if (!rnm.noteMap.iterator().hasNext()) {
return null;
}
ObjectId treeId = rnm.noteMap.writeTree(ins);
- if (!rnm.noteMap.iterator().hasNext()) {
- logger.atSevere().log(
- "building draft update without content: hasComments=%s "
- + "touchedAllRevs=%s updateCommits=%d revisionNotes=%d",
- hasComments, touchedAllRevs, updatedCommits.size(), rnm.revisionNotes.size());
- }
cb.setTreeId(treeId);
return cb;
}
diff --git a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 6871652..15fa0f4 100644
--- a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -22,6 +22,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -45,7 +46,9 @@
@Override
protected void configure() {
factory(PatchListLoader.Factory.class);
- persist(FILE_NAME, PatchListKey.class, PatchList.class)
+ // TODO(davido): Switch off using legacy cache backend, after fixing PatchListLoader
+ // to be recursion free.
+ persist(FILE_NAME, PatchListKey.class, PatchList.class, CacheBackend.GUAVA)
.maximumWeight(10 << 20)
.weigher(PatchListWeigher.class);
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 06fe471..378a512 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -440,8 +440,7 @@
@Override
public ForChange change(ChangeData cd) {
try {
- // TODO(hiesel) Force callers to call database() and use db instead of cd.db()
- return getProjectControl().controlFor(cd.change()).asForChange(cd);
+ return getProjectControl().controlFor(cd.notes()).asForChange(cd);
} catch (StorageException e) {
return FailedPermissionBackend.change("unavailable", e);
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index c6beac4..78ca0fc 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -598,7 +598,11 @@
committer = c.getCommitterIdent();
parentCount = c.getParentCount();
} catch (IOException e) {
- throw new StorageException(e);
+ throw new StorageException(
+ String.format(
+ "Loading commit %s for ps %d of change %d failed.",
+ ps.commitId(), ps.id().get(), ps.id().changeId().get()),
+ e);
}
return true;
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index c86bd94..61b90f1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -63,6 +63,7 @@
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.OperatorAliasConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeField;
@@ -94,6 +95,7 @@
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
/** Parses a query string meant to be applied to change objects. */
@@ -221,6 +223,7 @@
final GroupMembers groupMembers;
final Provider<AnonymousUser> anonymousUserProvider;
final OperatorAliasConfig operatorAliasConfig;
+ final boolean indexMergeable;
private final Provider<CurrentUser> self;
@@ -253,7 +256,8 @@
AccountCache accountCache,
GroupMembers groupMembers,
Provider<AnonymousUser> anonymousUserProvider,
- OperatorAliasConfig operatorAliasConfig) {
+ OperatorAliasConfig operatorAliasConfig,
+ @GerritServerConfig Config gerritConfig) {
this(
queryProvider,
rewriter,
@@ -281,7 +285,8 @@
accountCache,
groupMembers,
anonymousUserProvider,
- operatorAliasConfig);
+ operatorAliasConfig,
+ gerritConfig.getBoolean("index", "change", "indexMergeable", true));
}
private Arguments(
@@ -311,7 +316,8 @@
AccountCache accountCache,
GroupMembers groupMembers,
Provider<AnonymousUser> anonymousUserProvider,
- OperatorAliasConfig operatorAliasConfig) {
+ OperatorAliasConfig operatorAliasConfig,
+ boolean indexMergeable) {
this.queryProvider = queryProvider;
this.rewriter = rewriter;
this.opFactories = opFactories;
@@ -339,6 +345,7 @@
this.groupMembers = groupMembers;
this.anonymousUserProvider = anonymousUserProvider;
this.operatorAliasConfig = operatorAliasConfig;
+ this.indexMergeable = indexMergeable;
}
Arguments asUser(CurrentUser otherUser) {
@@ -369,7 +376,8 @@
accountCache,
groupMembers,
anonymousUserProvider,
- operatorAliasConfig);
+ operatorAliasConfig,
+ indexMergeable);
}
Arguments asUser(Account.Id otherId) {
@@ -570,6 +578,9 @@
}
if ("mergeable".equalsIgnoreCase(value)) {
+ if (!args.indexMergeable) {
+ throw new QueryParseException("server does not support 'mergeable'. check configs");
+ }
return new BooleanPredicate(ChangeField.MERGEABLE);
}
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
index b92a464..65a7f4f 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -14,10 +14,13 @@
package com.google.gerrit.server.restapi.group;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NeedsParams;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestView;
@@ -30,21 +33,27 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
-public class GroupsCollection implements RestCollection<TopLevelResource, GroupResource> {
+public class GroupsCollection
+ implements RestCollection<TopLevelResource, GroupResource>, NeedsParams {
private final DynamicMap<RestView<GroupResource>> views;
+ private final Provider<ListGroups> list;
private final Provider<QueryGroups> queryGroups;
private final GroupControl.Factory groupControlFactory;
private final GroupResolver groupResolver;
private final Provider<CurrentUser> self;
+ private boolean hasQuery;
+
@Inject
public GroupsCollection(
DynamicMap<RestView<GroupResource>> views,
+ Provider<ListGroups> list,
Provider<QueryGroups> queryGroups,
GroupControl.Factory groupControlFactory,
GroupResolver groupResolver,
Provider<CurrentUser> self) {
this.views = views;
+ this.list = list;
this.queryGroups = queryGroups;
this.groupControlFactory = groupControlFactory;
this.groupResolver = groupResolver;
@@ -52,6 +61,11 @@
}
@Override
+ public void setParams(ListMultimap<String, String> params) throws BadRequestException {
+ this.hasQuery = params.containsKey("query");
+ }
+
+ @Override
public RestView<TopLevelResource> list() throws ResourceNotFoundException, AuthException {
final CurrentUser user = self.get();
if (user instanceof AnonymousUser) {
@@ -59,7 +73,12 @@
} else if (!(user.isIdentifiedUser())) {
throw new ResourceNotFoundException();
}
- return queryGroups.get();
+
+ if (hasQuery) {
+ return queryGroups.get();
+ }
+
+ return list.get();
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index 03b9452..5d51527 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -91,85 +91,7 @@
try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
ProjectConfig config = projectConfigFactory.read(md);
- if (config.getLabelSections().containsKey(id.get())) {
- throw new ResourceConflictException(String.format("label %s already exists", id.get()));
- }
-
- for (String labelName : config.getLabelSections().keySet()) {
- if (labelName.equalsIgnoreCase(id.get())) {
- throw new ResourceConflictException(
- String.format("label %s conflicts with existing label %s", id.get(), labelName));
- }
- }
-
- if (input.values == null || input.values.isEmpty()) {
- throw new BadRequestException("values are required");
- }
-
- List<LabelValue> values = LabelDefinitionInputParser.parseValues(input.values);
-
- LabelType labelType;
- try {
- labelType = new LabelType(id.get(), values);
- } catch (IllegalArgumentException e) {
- throw new BadRequestException("invalid name: " + id.get(), e);
- }
-
- if (input.function != null && !input.function.trim().isEmpty()) {
- labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
- } else {
- labelType.setFunction(LabelFunction.MAX_WITH_BLOCK);
- }
-
- if (input.defaultValue != null) {
- labelType.setDefaultValue(
- LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
- }
-
- if (input.branches != null) {
- labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
- }
-
- if (input.canOverride != null) {
- labelType.setCanOverride(input.canOverride);
- }
-
- if (input.copyAnyScore != null) {
- labelType.setCopyAnyScore(input.copyAnyScore);
- }
-
- if (input.copyMinScore != null) {
- labelType.setCopyMinScore(input.copyMinScore);
- }
-
- if (input.copyMaxScore != null) {
- labelType.setCopyMaxScore(input.copyMaxScore);
- }
-
- if (input.copyAllScoresIfNoChange != null) {
- labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
- }
-
- if (input.copyAllScoresIfNoCodeChange != null) {
- labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
- }
-
- if (input.copyAllScoresOnTrivialRebase != null) {
- labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
- }
-
- if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
- labelType.setCopyAllScoresOnMergeFirstParentUpdate(
- input.copyAllScoresOnMergeFirstParentUpdate);
- }
-
- if (input.allowPostSubmit != null) {
- labelType.setAllowPostSubmit(input.allowPostSubmit);
- }
-
- if (input.ignoreSelfApproval != null) {
- labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
- }
+ LabelType labelType = createLabel(config, id.get(), input);
if (input.commitMessage != null) {
md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
@@ -177,7 +99,6 @@
md.setMessage("Update label");
}
- config.getLabelSections().put(labelType.getName(), labelType);
config.commit(md);
projectCache.evict(rsrc.getProjectState().getProject());
@@ -185,4 +106,101 @@
return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType));
}
}
+
+ /**
+ * Creates a new label.
+ *
+ * @param config the project config
+ * @param label the name of the new label
+ * @param input the input that describes the new label
+ * @return the created label type
+ * @throws BadRequestException if there was invalid data in the input
+ * @throws ResourceConflictException if the label cannot be created due to a conflict
+ */
+ public LabelType createLabel(ProjectConfig config, String label, LabelDefinitionInput input)
+ throws BadRequestException, ResourceConflictException {
+ if (config.getLabelSections().containsKey(label)) {
+ throw new ResourceConflictException(String.format("label %s already exists", label));
+ }
+
+ for (String labelName : config.getLabelSections().keySet()) {
+ if (labelName.equalsIgnoreCase(label)) {
+ throw new ResourceConflictException(
+ String.format("label %s conflicts with existing label %s", label, labelName));
+ }
+ }
+
+ if (input.values == null || input.values.isEmpty()) {
+ throw new BadRequestException("values are required");
+ }
+
+ List<LabelValue> values = LabelDefinitionInputParser.parseValues(input.values);
+
+ LabelType labelType;
+ try {
+ labelType = new LabelType(label, values);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("invalid name: " + label, e);
+ }
+
+ if (input.function != null && !input.function.trim().isEmpty()) {
+ labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
+ } else {
+ labelType.setFunction(LabelFunction.MAX_WITH_BLOCK);
+ }
+
+ if (input.defaultValue != null) {
+ labelType.setDefaultValue(
+ LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
+ }
+
+ if (input.branches != null) {
+ labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
+ }
+
+ if (input.canOverride != null) {
+ labelType.setCanOverride(input.canOverride);
+ }
+
+ if (input.copyAnyScore != null) {
+ labelType.setCopyAnyScore(input.copyAnyScore);
+ }
+
+ if (input.copyMinScore != null) {
+ labelType.setCopyMinScore(input.copyMinScore);
+ }
+
+ if (input.copyMaxScore != null) {
+ labelType.setCopyMaxScore(input.copyMaxScore);
+ }
+
+ if (input.copyAllScoresIfNoChange != null) {
+ labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
+ }
+
+ if (input.copyAllScoresIfNoCodeChange != null) {
+ labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
+ }
+
+ if (input.copyAllScoresOnTrivialRebase != null) {
+ labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
+ }
+
+ if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(
+ input.copyAllScoresOnMergeFirstParentUpdate);
+ }
+
+ if (input.allowPostSubmit != null) {
+ labelType.setAllowPostSubmit(input.allowPostSubmit);
+ }
+
+ if (input.ignoreSelfApproval != null) {
+ labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
+ }
+
+ config.getLabelSections().put(labelType.getName(), labelType);
+
+ return labelType;
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
index 5464abf..531640c 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
@@ -77,12 +77,10 @@
try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
ProjectConfig config = projectConfigFactory.read(md);
- if (!config.getLabelSections().containsKey(rsrc.getLabelType().getName())) {
+ if (!deleteLabel(config, rsrc.getLabelType().getName())) {
throw new ResourceNotFoundException(IdString.fromDecoded(rsrc.getLabelType().getName()));
}
- config.getLabelSections().remove(rsrc.getLabelType().getName());
-
if (input.commitMessage != null) {
md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
} else {
@@ -96,4 +94,20 @@
return Response.none();
}
+
+ /**
+ * Delete the given label from the given project config.
+ *
+ * @param config the project config from which the label should be deleted
+ * @param labelName the name of the label that should be deleted
+ * @return {@code true} if the label was deleted, {@code false} if the label was not found
+ */
+ public boolean deleteLabel(ProjectConfig config, String labelName) {
+ if (!config.getLabelSections().containsKey(labelName)) {
+ return false;
+ }
+
+ config.getLabelSections().remove(labelName);
+ return true;
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 2c76cbd..5b3ea30 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -72,6 +72,7 @@
get(LABEL_KIND).to(GetLabel.class);
put(LABEL_KIND).to(SetLabel.class);
delete(LABEL_KIND).to(DeleteLabel.class);
+ postOnCollection(LABEL_KIND).to(PostLabels.class);
get(PROJECT_KIND, "HEAD").to(GetHead.class);
put(PROJECT_KIND, "HEAD").to(SetHead.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabels.java b/java/com/google/gerrit/server/restapi/project/PostLabels.java
new file mode 100644
index 0000000..8835359
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/PostLabels.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2019 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.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.common.BatchLabelInput;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Map.Entry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** REST endpoint that allows to add, update and delete label definitions in a batch. */
+@Singleton
+public class PostLabels
+ implements RestCollectionModifyView<ProjectResource, LabelResource, BatchLabelInput> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final MetaDataUpdate.User updateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final DeleteLabel deleteLabel;
+ private final CreateLabel createLabel;
+ private final SetLabel setLabel;
+ private final ProjectCache projectCache;
+
+ @Inject
+ public PostLabels(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ MetaDataUpdate.User updateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ DeleteLabel deleteLabel,
+ CreateLabel createLabel,
+ SetLabel setLabel,
+ ProjectCache projectCache) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.deleteLabel = deleteLabel;
+ this.createLabel = createLabel;
+ this.setLabel = setLabel;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public Response<?> apply(ProjectResource rsrc, BatchLabelInput input)
+ throws AuthException, UnprocessableEntityException, PermissionBackendException, IOException,
+ ConfigInvalidException, BadRequestException, ResourceConflictException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ if (input == null) {
+ input = new BatchLabelInput();
+ }
+
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
+ boolean dirty = false;
+
+ ProjectConfig config = projectConfigFactory.read(md);
+
+ if (input.delete != null && !input.delete.isEmpty()) {
+ for (String labelName : input.delete) {
+ if (!deleteLabel.deleteLabel(config, labelName.trim())) {
+ throw new UnprocessableEntityException(String.format("label %s not found", labelName));
+ }
+ }
+ dirty = true;
+ }
+
+ if (input.create != null && !input.create.isEmpty()) {
+ for (LabelDefinitionInput labelInput : input.create) {
+ if (labelInput.name == null || labelInput.name.trim().isEmpty()) {
+ throw new BadRequestException("label name is required for new label");
+ }
+ if (labelInput.commitMessage != null) {
+ throw new BadRequestException("commit message on label definition input not supported");
+ }
+ createLabel.createLabel(config, labelInput.name.trim(), labelInput);
+ }
+ dirty = true;
+ }
+
+ if (input.update != null && !input.update.isEmpty()) {
+ for (Entry<String, LabelDefinitionInput> e : input.update.entrySet()) {
+ LabelType labelType = config.getLabelSections().get(e.getKey().trim());
+ if (labelType == null) {
+ throw new UnprocessableEntityException(String.format("label %s not found", e.getKey()));
+ }
+ if (e.getValue().commitMessage != null) {
+ throw new BadRequestException("commit message on label definition input not supported");
+ }
+ setLabel.updateLabel(config, labelType, e.getValue());
+ }
+ dirty = true;
+ }
+
+ if (input.commitMessage != null) {
+ md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
+ } else {
+ md.setMessage("Update labels");
+ }
+
+ if (dirty) {
+ config.commit(md);
+ projectCache.evict(rsrc.getProjectState().getProject());
+ }
+ }
+
+ return Response.ok("");
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index b7cffce..824b4ed 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -80,117 +80,9 @@
LabelType labelType = rsrc.getLabelType();
try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
- boolean dirty = false;
-
ProjectConfig config = projectConfigFactory.read(md);
- config.getLabelSections().remove(labelType.getName());
- if (input.name != null) {
- String newName = input.name.trim();
- if (newName.isEmpty()) {
- throw new BadRequestException("name cannot be empty");
- }
- if (!newName.equals(labelType.getName())) {
- if (config.getLabelSections().containsKey(newName)) {
- throw new ResourceConflictException(String.format("name %s already in use", newName));
- }
-
- for (String labelName : config.getLabelSections().keySet()) {
- if (labelName.equalsIgnoreCase(newName)) {
- throw new ResourceConflictException(
- String.format("name %s conflicts with existing label %s", newName, labelName));
- }
- }
-
- try {
- labelType.setName(newName);
- } catch (IllegalArgumentException e) {
- throw new BadRequestException("invalid name: " + input.name, e);
- }
- dirty = true;
- }
- }
-
- if (input.function != null) {
- if (input.function.trim().isEmpty()) {
- throw new BadRequestException("function cannot be empty");
- }
- labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
- dirty = true;
- }
-
- if (input.values != null) {
- if (input.values.isEmpty()) {
- throw new BadRequestException("values cannot be empty");
- }
- labelType.setValues(LabelDefinitionInputParser.parseValues(input.values));
- dirty = true;
- }
-
- if (input.defaultValue != null) {
- labelType.setDefaultValue(
- LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
- dirty = true;
- }
-
- if (input.branches != null) {
- labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
- dirty = true;
- }
-
- if (input.canOverride != null) {
- labelType.setCanOverride(input.canOverride);
- dirty = true;
- }
-
- if (input.copyAnyScore != null) {
- labelType.setCopyAnyScore(input.copyAnyScore);
- dirty = true;
- }
-
- if (input.copyMinScore != null) {
- labelType.setCopyMinScore(input.copyMinScore);
- dirty = true;
- }
-
- if (input.copyMaxScore != null) {
- labelType.setCopyMaxScore(input.copyMaxScore);
- dirty = true;
- }
-
- if (input.copyAllScoresIfNoChange != null) {
- labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
- }
-
- if (input.copyAllScoresIfNoCodeChange != null) {
- labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
- dirty = true;
- }
-
- if (input.copyAllScoresOnTrivialRebase != null) {
- labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
- dirty = true;
- }
-
- if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
- labelType.setCopyAllScoresOnMergeFirstParentUpdate(
- input.copyAllScoresOnMergeFirstParentUpdate);
- dirty = true;
- }
-
- if (input.allowPostSubmit != null) {
- labelType.setAllowPostSubmit(input.allowPostSubmit);
- dirty = true;
- }
-
- if (input.ignoreSelfApproval != null) {
- labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
- dirty = true;
- }
-
- if (dirty) {
- config.getLabelSections().put(labelType.getName(), labelType);
-
+ if (updateLabel(config, labelType, input)) {
if (input.commitMessage != null) {
md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
} else {
@@ -203,4 +95,128 @@
}
return Response.ok(LabelDefinitionJson.format(rsrc.getProject().getNameKey(), labelType));
}
+
+ /**
+ * Updates the given label.
+ *
+ * @param config the project config
+ * @param labelType the label type that should be updated
+ * @param input the input that describes the label update
+ * @return whether the label type was modified
+ * @throws BadRequestException if there was invalid data in the input
+ * @throws ResourceConflictException if the update cannot be applied due to a conflict
+ */
+ public boolean updateLabel(ProjectConfig config, LabelType labelType, LabelDefinitionInput input)
+ throws BadRequestException, ResourceConflictException {
+ boolean dirty = false;
+
+ config.getLabelSections().remove(labelType.getName());
+
+ if (input.name != null) {
+ String newName = input.name.trim();
+ if (newName.isEmpty()) {
+ throw new BadRequestException("name cannot be empty");
+ }
+ if (!newName.equals(labelType.getName())) {
+ if (config.getLabelSections().containsKey(newName)) {
+ throw new ResourceConflictException(String.format("name %s already in use", newName));
+ }
+
+ for (String labelName : config.getLabelSections().keySet()) {
+ if (labelName.equalsIgnoreCase(newName)) {
+ throw new ResourceConflictException(
+ String.format("name %s conflicts with existing label %s", newName, labelName));
+ }
+ }
+
+ try {
+ labelType.setName(newName);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("invalid name: " + input.name, e);
+ }
+ dirty = true;
+ }
+ }
+
+ if (input.function != null) {
+ if (input.function.trim().isEmpty()) {
+ throw new BadRequestException("function cannot be empty");
+ }
+ labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
+ dirty = true;
+ }
+
+ if (input.values != null) {
+ if (input.values.isEmpty()) {
+ throw new BadRequestException("values cannot be empty");
+ }
+ labelType.setValues(LabelDefinitionInputParser.parseValues(input.values));
+ dirty = true;
+ }
+
+ if (input.defaultValue != null) {
+ labelType.setDefaultValue(
+ LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
+ dirty = true;
+ }
+
+ if (input.branches != null) {
+ labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
+ dirty = true;
+ }
+
+ if (input.canOverride != null) {
+ labelType.setCanOverride(input.canOverride);
+ dirty = true;
+ }
+
+ if (input.copyAnyScore != null) {
+ labelType.setCopyAnyScore(input.copyAnyScore);
+ dirty = true;
+ }
+
+ if (input.copyMinScore != null) {
+ labelType.setCopyMinScore(input.copyMinScore);
+ dirty = true;
+ }
+
+ if (input.copyMaxScore != null) {
+ labelType.setCopyMaxScore(input.copyMaxScore);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresIfNoChange != null) {
+ labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
+ }
+
+ if (input.copyAllScoresIfNoCodeChange != null) {
+ labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresOnTrivialRebase != null) {
+ labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(
+ input.copyAllScoresOnMergeFirstParentUpdate);
+ dirty = true;
+ }
+
+ if (input.allowPostSubmit != null) {
+ labelType.setAllowPostSubmit(input.allowPostSubmit);
+ dirty = true;
+ }
+
+ if (input.ignoreSelfApproval != null) {
+ labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
+ dirty = true;
+ }
+
+ config.getLabelSections().put(labelType.getName(), labelType);
+
+ return dirty;
+ }
}
diff --git a/java/com/google/gerrit/testing/IndexConfig.java b/java/com/google/gerrit/testing/IndexConfig.java
index 21c49dd..fb6c926 100644
--- a/java/com/google/gerrit/testing/IndexConfig.java
+++ b/java/com/google/gerrit/testing/IndexConfig.java
@@ -23,7 +23,9 @@
public static Config createFromExistingConfig(Config cfg) {
cfg.setInt("index", null, "maxPages", 10);
- cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
+ // To avoid this flakiness indexMergeable is switched off for the tests as it incurs background
+ // reindex calls.
+ cfg.setBoolean("index", "change", "indexMergeable", false);
cfg.setString("trackingid", "query-bug", "footer", "Bug:");
cfg.setString("trackingid", "query-bug", "match", "QUERY\\d{2,8}");
cfg.setString("trackingid", "query-bug", "system", "querytests");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index e38babb..7ecb07e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4396,6 +4396,20 @@
}
}
+ @Test
+ @GerritConfig(name = "index.change.indexMergeable", value = "true")
+ public void changeQueryReturnsMergeableWhenGerritIndexMergeable() throws Exception {
+ String changeId = createChange().getChangeId();
+ assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isTrue();
+ }
+
+ @Test
+ @GerritConfig(name = "index.change.indexMergeable", value = "false")
+ public void changeQueryDoesNotReturnMergeableWhenGerritDoesNotIndexMergeable() throws Exception {
+ String changeId = createChange().getChangeId();
+ assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isNull();
+ }
+
private PushOneCommit.Result createWorkInProgressChange() throws Exception {
return pushTo("refs/for/master%wip");
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java
index 54c5d05..b5d673c 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangeIT.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.util.stream.Collectors.toList;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -182,6 +183,18 @@
.containsExactly(changeId3, changeId4);
}
+ @Test
+ public void usingOutOfRangeLabelValuesDoesNotCauseError() throws Exception {
+ for (String operator : ImmutableList.of("=", ">", ">=", "<", "<=")) {
+ QueryChanges queryChanges = queryChangesProvider.get();
+ queryChanges.addQuery("label:Code-Review" + operator + "10");
+ queryChanges.addQuery("label:Code-Review" + operator + "-10");
+ queryChanges.addQuery("Code-Review" + operator + "10");
+ queryChanges.addQuery("Code-Review" + operator + "-10");
+ assertThat(queryChanges.apply(TopLevelResource.INSTANCE).statusCode()).isEqualTo(SC_OK);
+ }
+ }
+
private static void assertNoChangeHasMoreChangesSet(List<ChangeInfo> results) {
for (ChangeInfo info : results) {
assertThat(info._moreChanges).isNull();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index ad73e0f..61d0fd5 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -46,6 +46,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.Permission;
@@ -1062,6 +1063,7 @@
}
@Test
+ @GerritConfig(name = "index.change.indexMergeable", value = "true")
public void mergeable() throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index f0312de..d1d197b 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -1446,7 +1446,7 @@
private TestRefAdvertiser.Result getReceivePackRefs() throws Exception {
try (Repository repo = repoManager.openRepository(project)) {
AdvertiseRefsHook adv =
- ReceiveCommitsAdvertiseRefsHookChain.createForTest(queryProvider, project);
+ ReceiveCommitsAdvertiseRefsHookChain.createForTest(queryProvider, project, admin.id());
ReceivePack rp = new ReceivePack(repo);
rp.setAdvertiseRefsHook(adv);
TestRefAdvertiser advertiser = new TestRefAdvertiser(repo);
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index b8ab752..55eeaf4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -83,7 +83,8 @@
.expectedResponseCode(SC_NOT_FOUND)
.build(),
RestCall.get("/projects/%s/dashboards"),
- RestCall.put("/projects/%s/labels/new-label"));
+ RestCall.put("/projects/%s/labels/new-label"),
+ RestCall.post("/projects/%s/labels/"));
/**
* Child project REST endpoints to be tested, each URL contains placeholders for the parent
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index aaf52a1..eff98b3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1205,7 +1205,7 @@
}
@Test
- @GerritConfig(name = "index.reindexAfterRefUpdate", value = "true")
+ @GerritConfig(name = "index.change.indexMergeable", value = "true")
public void submitSchedulesOpenChangesOfSameBranchForReindexing() throws Throwable {
// Create a merged change.
PushOneCommit push =
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 3030b02..ea3a6a0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -22,6 +22,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -58,6 +59,7 @@
}
@Test
+ @GerritConfig(name = "index.change.indexMergeable", value = "true")
public void indexChangeAfterOwnerLosesVisibility() throws Exception {
// Create a test group with 2 users as members
TestAccount user2 = accountCreator.user2();
diff --git a/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
new file mode 100644
index 0000000..d8132b7
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 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.rest.group;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gson.reflect.TypeToken;
+import java.util.Map;
+import org.junit.Test;
+
+public class ListGroupsIT extends AbstractDaemonTest {
+ @Test
+ public void listAllGroups() throws Exception {
+ RestResponse response = adminRestSession.get("/groups/");
+ response.assertOK();
+
+ Map<String, GroupInfo> groupMap =
+ newGson()
+ .fromJson(response.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+ assertThat(groupMap.keySet()).containsExactly("Administrators", "Non-Interactive Users");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
new file mode 100644
index 0000000..9e6b051
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
@@ -0,0 +1,456 @@
+// Copyright (C) 2019 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.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.BatchLabelInput;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.restapi.project.PostLabels;
+import com.google.inject.Inject;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/** Tests for the {@link PostLabels} REST endpoint. */
+public class PostLabelsIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(allProjects.get()).labels(new BatchLabelInput()));
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(allProjects.get()).labels(new BatchLabelInput()));
+ assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+ }
+
+ @Test
+ public void deleteNonExistingLabel() throws Exception {
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Foo");
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(allProjects.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Foo not found");
+ }
+
+ @Test
+ public void deleteLabels() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+ configLabel("Bar", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).labels().get()).isNotEmpty();
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Foo", "Bar");
+ gApi.projects().name(project.get()).labels(input);
+ assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty();
+ }
+
+ @Test
+ public void deleteLabels_labelNamesAreTrimmed() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+ configLabel("Bar", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).labels().get()).isNotEmpty();
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of(" Foo ", " Bar ");
+ gApi.projects().name(project.get()).labels(input);
+ assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty();
+ }
+
+ @Test
+ public void cannotDeleteTheSameLabelTwice() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Foo", "Foo");
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(project.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Foo not found");
+ }
+
+ @Test
+ public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception {
+ LabelDefinitionInput labelInput = new LabelDefinitionInput();
+ labelInput.name = "Code-Review";
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(labelInput);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(allProjects.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Code-Review already exists");
+ }
+
+ @Test
+ public void cannotCreateTwoLabelsWithTheSameName() throws Exception {
+ LabelDefinitionInput fooInput = new LabelDefinitionInput();
+ fooInput.name = "Foo";
+ fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(fooInput, fooInput);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Foo already exists");
+ }
+
+ @Test
+ public void cannotCreateTwoLabelsWithNamesThatAreTheSameAfterTrim() throws Exception {
+ LabelDefinitionInput foo1Input = new LabelDefinitionInput();
+ foo1Input.name = "Foo";
+ foo1Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInput foo2Input = new LabelDefinitionInput();
+ foo2Input.name = " Foo ";
+ foo2Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(foo1Input, foo2Input);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Foo already exists");
+ }
+
+ @Test
+ public void cannotCreateTwoLabelsWithConflictingNames() throws Exception {
+ LabelDefinitionInput foo1Input = new LabelDefinitionInput();
+ foo1Input.name = "Foo";
+ foo1Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInput foo2Input = new LabelDefinitionInput();
+ foo2Input.name = "foo";
+ foo2Input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(foo1Input, foo2Input);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label foo conflicts with existing label Foo");
+ }
+
+ @Test
+ public void createLabels() throws Exception {
+ LabelDefinitionInput fooInput = new LabelDefinitionInput();
+ fooInput.name = "Foo";
+ fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInput barInput = new LabelDefinitionInput();
+ barInput.name = "Bar";
+ barInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(fooInput, barInput);
+
+ gApi.projects().name(allProjects.get()).labels(input);
+ assertThat(gApi.projects().name(allProjects.get()).label("Foo").get()).isNotNull();
+ assertThat(gApi.projects().name(allProjects.get()).label("Bar").get()).isNotNull();
+ }
+
+ @Test
+ public void createLabels_labelNamesAreTrimmed() throws Exception {
+ LabelDefinitionInput fooInput = new LabelDefinitionInput();
+ fooInput.name = " Foo ";
+ fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInput barInput = new LabelDefinitionInput();
+ barInput.name = " Bar ";
+ barInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(fooInput, barInput);
+
+ gApi.projects().name(allProjects.get()).labels(input);
+ assertThat(gApi.projects().name(allProjects.get()).label("Foo").get()).isNotNull();
+ assertThat(gApi.projects().name(allProjects.get()).label("Bar").get()).isNotNull();
+ }
+
+ @Test
+ public void cannotCreateLabelWithoutName() throws Exception {
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(new LabelDefinitionInput());
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label name is required for new label");
+ }
+
+ @Test
+ public void cannotSetCommitMessageOnLabelDefinitionInputForCreate() throws Exception {
+ LabelDefinitionInput labelInput = new LabelDefinitionInput();
+ labelInput.name = "Foo";
+ labelInput.commitMessage = "Create Label Foo";
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(labelInput);
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("commit message on label definition input not supported");
+ }
+
+ @Test
+ public void updateNonExistingLabel() throws Exception {
+ BatchLabelInput input = new BatchLabelInput();
+ input.update = ImmutableMap.of("Foo", new LabelDefinitionInput());
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(allProjects.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Foo not found");
+ }
+
+ @Test
+ public void updateLabels() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+ configLabel("Bar", LabelFunction.NO_OP);
+
+ LabelDefinitionInput fooUpdate = new LabelDefinitionInput();
+ fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName();
+ LabelDefinitionInput barUpdate = new LabelDefinitionInput();
+ barUpdate.name = "Baz";
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.update = ImmutableMap.of("Foo", fooUpdate, "Bar", barUpdate);
+
+ gApi.projects().name(project.get()).labels(input);
+
+ assertThat(gApi.projects().name(project.get()).label("Foo").get().function)
+ .isEqualTo(fooUpdate.function);
+ assertThat(gApi.projects().name(project.get()).label("Baz").get()).isNotNull();
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).label("Bar").get());
+ }
+
+ @Test
+ public void updateLabels_labelNamesAreTrimmed() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+ configLabel("Bar", LabelFunction.NO_OP);
+
+ LabelDefinitionInput fooUpdate = new LabelDefinitionInput();
+ fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName();
+ LabelDefinitionInput barUpdate = new LabelDefinitionInput();
+ barUpdate.name = "Baz";
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.update = ImmutableMap.of(" Foo ", fooUpdate, " Bar ", barUpdate);
+
+ gApi.projects().name(project.get()).labels(input);
+
+ assertThat(gApi.projects().name(project.get()).label("Foo").get().function)
+ .isEqualTo(fooUpdate.function);
+ assertThat(gApi.projects().name(project.get()).label("Baz").get()).isNotNull();
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).label("Bar").get());
+ }
+
+ @Test
+ public void cannotSetCommitMessageOnLabelDefinitionInputForUpdate() throws Exception {
+ LabelDefinitionInput labelInput = new LabelDefinitionInput();
+ labelInput.commitMessage = "Update label";
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.update = ImmutableMap.of("Code-Review", labelInput);
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class, () -> gApi.projects().name(allProjects.get()).labels(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("commit message on label definition input not supported");
+ }
+
+ @Test
+ public void deleteAndRecreateLabel() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+
+ LabelDefinitionInput fooInput = new LabelDefinitionInput();
+ fooInput.name = "Foo";
+ fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+ fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Foo");
+ input.create = ImmutableList.of(fooInput);
+
+ gApi.projects().name(project.get()).labels(input);
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get();
+ assertThat(fooLabel.function).isEqualTo(fooInput.function);
+ }
+
+ @Test
+ public void deleteRecreateAndUpdateLabel() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+
+ LabelDefinitionInput fooCreateInput = new LabelDefinitionInput();
+ fooCreateInput.name = "Foo";
+ fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+ fooCreateInput.values =
+ ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput();
+ fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName();
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Foo");
+ input.create = ImmutableList.of(fooCreateInput);
+ input.update = ImmutableMap.of("Foo", fooUpdateInput);
+
+ gApi.projects().name(project.get()).labels(input);
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get();
+ assertThat(fooLabel.function).isEqualTo(fooUpdateInput.function);
+ }
+
+ @Test
+ public void cannotDeleteAndUpdateLabel() throws Exception {
+ configLabel("Foo", LabelFunction.NO_OP);
+
+ LabelDefinitionInput fooInput = new LabelDefinitionInput();
+ fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Foo");
+ input.update = ImmutableMap.of("Foo", fooInput);
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(project.get()).labels(input));
+ assertThat(thrown).hasMessageThat().contains("label Foo not found");
+ }
+
+ @Test
+ public void createAndUpdateLabel() throws Exception {
+ LabelDefinitionInput fooCreateInput = new LabelDefinitionInput();
+ fooCreateInput.name = "Foo";
+ fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+ fooCreateInput.values =
+ ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput();
+ fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName();
+
+ BatchLabelInput input = new BatchLabelInput();
+ input.create = ImmutableList.of(fooCreateInput);
+ input.update = ImmutableMap.of("Foo", fooUpdateInput);
+
+ gApi.projects().name(project.get()).labels(input);
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("Foo").get();
+ assertThat(fooLabel.function).isEqualTo(fooUpdateInput.function);
+ }
+
+ @Test
+ public void noOpUpdate() throws Exception {
+ RevCommit refsMetaConfigHead =
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG);
+
+ gApi.projects().name(allProjects.get()).labels(new BatchLabelInput());
+
+ assertThat(projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG))
+ .isEqualTo(refsMetaConfigHead);
+ }
+
+ @Test
+ public void defaultCommitMessage() throws Exception {
+ BatchLabelInput input = new BatchLabelInput();
+ input.delete = ImmutableList.of("Code-Review");
+ gApi.projects().name(allProjects.get()).labels(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Update labels");
+ }
+
+ @Test
+ public void withCommitMessage() throws Exception {
+ BatchLabelInput input = new BatchLabelInput();
+ input.commitMessage = "Batch Update Labels";
+ input.delete = ImmutableList.of("Code-Review");
+ gApi.projects().name(allProjects.get()).labels(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo(input.commitMessage);
+ }
+
+ @Test
+ public void commitMessageIsTrimmed() throws Exception {
+ BatchLabelInput input = new BatchLabelInput();
+ input.commitMessage = " Batch Update Labels ";
+ input.delete = ImmutableList.of("Code-Review");
+ gApi.projects().name(allProjects.get()).labels(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Batch Update Labels");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index b32ce35..65f8aa0 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -29,9 +29,11 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -68,6 +70,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -81,6 +84,7 @@
@Inject private Provider<ChangesCollection> changes;
@Inject private Provider<PostReview> postReview;
@Inject private RequestScopeOperations requestScopeOperations;
+ @Inject ProjectOperations projectOperations;
private final Integer[] lines = {0, 1};
@@ -314,6 +318,47 @@
}
@Test
+ public void postCommentsUnreachableData() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+
+ String file = "file";
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "first subject", file, "l1\nl2\n");
+
+ String dest = "refs/for/master";
+ PushOneCommit.Result r1 = push.to(dest);
+ r1.assertOkStatus();
+ String changeId = r1.getChangeId();
+ String revId = r1.getCommit().getName();
+
+ PushOneCommit.Result r2 = amendChange(r1.getChangeId());
+ r2.assertOkStatus();
+
+ String draftRefName = RefNames.refsDraftComments(r1.getChange().getId(), admin.id());
+
+ DraftInput draft = newDraft(file, Side.REVISION, 1, "comment");
+ addDraft(changeId, "1", draft);
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.drafts = DraftHandling.PUBLISH;
+ reviewInput.message = "foo";
+ gApi.changes().id(r1.getChangeId()).revision(1).review(reviewInput);
+
+ addDraft(changeId, "2", newDraft(file, Side.REVISION, 2, "comment2"));
+ reviewInput = new ReviewInput();
+ reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ reviewInput.message = "bar";
+ gApi.changes().id(r1.getChangeId()).revision(2).review(reviewInput);
+
+ Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+ assertThat(drafts.isEmpty()).isTrue();
+
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Ref ref = repo.exactRef(draftRefName);
+ assertThat(ref).isNull();
+ }
+ }
+
+ @Test
public void listComments() throws Exception {
String file = "file";
PushOneCommit push =
diff --git a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index 7c78d61..b23f9a3 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -39,7 +39,9 @@
import com.google.gerrit.server.patch.Text;
import com.google.inject.Inject;
import com.google.inject.name.Named;
+import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
@@ -66,6 +68,25 @@
private Cache<PatchListKey, PatchList> abstractPatchListCache;
@Test
+ public void ensureLegacyBackendIsUsedForFileCacheBackend() throws Exception {
+ Field fileCacheField = patchListCache.getClass().getDeclaredField("fileCache");
+ fileCacheField.setAccessible(true);
+ // Use the reflection to access "localCache" field that is only present in Guava backend.
+ assertThat(
+ Arrays.stream(fileCacheField.get(patchListCache).getClass().getDeclaredFields())
+ .anyMatch(f -> f.getName().equals("localCache")))
+ .isTrue();
+
+ // intraCache (and all other cache backends) should use Caffeine backend.
+ Field intraCacheField = patchListCache.getClass().getDeclaredField("intraCache");
+ intraCacheField.setAccessible(true);
+ assertThat(
+ Arrays.stream(intraCacheField.get(patchListCache).getClass().getDeclaredFields())
+ .noneMatch(f -> f.getName().equals("localCache")))
+ .isTrue();
+ }
+
+ @Test
public void listPatchesAgainstBase() throws Exception {
commitBuilder().add(FILE_D, "4").message(SUBJECT_1).create();
pushHead(testRepo, "refs/heads/master", false);
diff --git a/javatests/com/google/gerrit/index/query/RangeUtilTest.java b/javatests/com/google/gerrit/index/query/RangeUtilTest.java
new file mode 100644
index 0000000..681f9d99b
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/RangeUtilTest.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2019 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.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.query.RangeUtil.Range;
+import org.junit.Test;
+
+public class RangeUtilTest {
+ @Test
+ public void getRangeForValueOutsideOfMinMaxRange_minNotGreaterThanMax() {
+ for (String operator : ImmutableList.of("=", ">", ">=", "<", "<=")) {
+ Range range = RangeUtil.getRange("foo", operator, 10, -4, 4);
+ assertThat(range.min).isAtMost(range.max);
+
+ range = RangeUtil.getRange("foo", operator, -10, -4, 4);
+ assertThat(range.min).isAtMost(range.max);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 9a48a68..3dfbefe 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -18,6 +18,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import org.eclipse.jgit.lib.Config;
import org.junit.Ignore;
@Ignore
@@ -26,8 +27,34 @@
super(
new ChangeQueryBuilder.Definition<>(FakeQueryBuilder.class),
new ChangeQueryBuilder.Arguments(
- null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, indexes, null, null, null, null, null, null, null, null));
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ indexes,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ new Config()));
}
@Operator
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 231340d..56b3aea 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -30,6 +30,7 @@
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;
import com.google.common.base.MoreObjects;
@@ -41,6 +42,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
@@ -79,6 +81,7 @@
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
@@ -2023,6 +2026,7 @@
}
@Test
+ @GerritConfig(name = "index.change.indexMergeable", value = "true")
public void mergeable() throws Exception {
TestRepository<Repo> repo = createProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
@@ -2040,7 +2044,7 @@
// If a change gets submitted, the remaining open changes get reindexed asynchronously to update
// their mergeability information. If the further assertions in this test are done before the
// asynchronous reindex completed they fail because the mergeability information in the index
- // was not updated yet. To avoid this flakiness reindexAfterRefUpdate is switched off for the
+ // was not updated yet. To avoid this flakiness indexMergeable is switched off for the
// tests and we index change2 synchronously here.
gApi.changes().id(change2.getChangeId()).index();
@@ -3079,6 +3083,20 @@
}
}
+ @Test
+ @GerritConfig(name = "index.change.indexMergeable", value = "false")
+ public void mergeableFailsWhenNotIndexed() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
+ insert(repo, newChangeForCommit(repo, commit1));
+
+ Throwable thrown = assertThrows(Throwable.class, () -> assertQuery("status:open is:mergeable"));
+ assertThat(thrown.getCause()).isInstanceOf(QueryParseException.class);
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("server does not support 'mergeable'. check configs");
+ }
+
protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
return newChange(repo, null, null, null, null, false);
}
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index d0162d3..e5b51e7 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -16,12 +16,14 @@
"//prolog:gerrit-prolog-common",
],
deps = [
+ "//java/com/google/gerrit/acceptance/config",
"//java/com/google/gerrit/acceptance/testsuite/project",
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/index",
+ "//java/com/google/gerrit/index:query_exception",
"//java/com/google/gerrit/lifecycle",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/project/testing:project-test-util",
diff --git a/lib/BUILD b/lib/BUILD
index 13580b1..39c622a 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -1,4 +1,4 @@
-load("@rules_java//java:defs.bzl", "java_library")
+load("@rules_java//java:defs.bzl", "java_import", "java_library")
exports_files(glob([
"LICENSE-*",
@@ -111,6 +111,29 @@
)
java_library(
+ name = "caffeine",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = [
+ "//java/com/google/gerrit/server/cache/mem:__pkg__",
+ ],
+ exports = ["@caffeine//jar"],
+)
+
+java_import(
+ name = "caffeine-guava-renamed",
+ jars = ["@caffeine-guava-renamed//file"],
+)
+
+java_library(
+ name = "caffeine-guava",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = [
+ "//java/com/google/gerrit/server/cache/mem:__pkg__",
+ ],
+ exports = [":caffeine-guava-renamed"],
+)
+
+java_library(
name = "jsch",
data = ["//lib:LICENSE-jsch"],
visibility = ["//visibility:public"],
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index ffc1b9b..1d6e706 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -43,11 +43,6 @@
.value {
width: 32em;
}
- gr-autocomplete {
- --gr-autocomplete: {
- padding: 0 var(--spacing-xs);
- }
- }
.hide {
display: none;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
index 8d03595..b78090c 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
@@ -40,14 +40,7 @@
width: 20em;
}
gr-autocomplete {
- border: none;
- --gr-autocomplete: {
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- height: 2em;
- padding: 0 var(--spacing-xs);
- width: 20em;
- }
+ width: 20em;
}
</style>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index bc7a109..cf24793 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -48,10 +48,6 @@
}
gr-autocomplete {
width: 20em;
- --gr-autocomplete: {
- height: 2em;
- width: 20em;
- }
}
a {
color: var(--primary-text-color);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 0cdef8c..1b57ddb 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -29,11 +29,9 @@
}
gr-autocomplete {
background-color: var(--view-background-color);
- border: 1px solid var(--border-color);
border-radius: var(--border-radius);
flex: 1;
outline: none;
- padding: var(--spacing-xs);
}
</style>
<form>
@@ -45,7 +43,6 @@
on-commit="_handleInputCommit"
allow-non-suggested-values
multi
- borderless
threshold="[[_threshold]]"
tab-complete
vertical-offset="30"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
index b7994e6..4009420 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock.js
@@ -17,16 +17,20 @@
(function() {
'use strict';
- Polymer({
- is: 'comment-api-mock',
+ class CommentApiMock extends Polymer.GestureEventListeners(
+ Polymer.LegacyElementMixin(
+ Polymer.Element)) {
+ static get is() { return 'comment-api-mock'; }
- properties: {
- _changeComments: Object,
- },
+ static get properties() {
+ return {
+ _changeComments: Object,
+ };
+ }
loadComments() {
return this._reloadComments();
- },
+ }
/**
* For the purposes of the mock, _reloadDrafts is not included because its
@@ -38,13 +42,15 @@
return this._reloadComments().then(() => {
return e.detail.resolve();
});
- },
+ }
_reloadComments() {
return this.$.commentAPI.loadAll(this._changeNum)
.then(comments => {
this._changeComments = this.$.commentAPI._changeComments;
});
- },
- });
+ }
+ }
+
+ customElements.define(CommentApiMock.is, CommentApiMock);
})();
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.html b/polygerrit-ui/app/elements/edit/gr-edit-constants.html
index d526ccd..5895124 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-constants.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.html
@@ -22,7 +22,7 @@
// Order corresponds to order in the UI.
GrEditConstants.Actions = {
- OPEN: {label: 'Open', id: 'open'},
+ OPEN: {label: 'Add/Open', id: 'open'},
DELETE: {label: 'Delete', id: 'delete'},
RENAME: {label: 'Rename', id: 'rename'},
RESTORE: {label: 'Restore', id: '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 52692a7..cb950da 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
@@ -55,21 +55,13 @@
gr-dialog .main > iron-input{
width: 100%;
}
- gr-autocomplete {
- --gr-autocomplete: {
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- height: 2em;
- padding: 0 var(--spacing-xs);
- }
- }
input {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
- height: 2em;
margin: var(--spacing-m) 0;
- padding: 0 var(--spacing-xs);
+ padding: var(--spacing-s);
width: 100%;
+ box-sizing: content-box;
}
@media screen and (max-width: 50em) {
gr-dialog {
@@ -89,12 +81,12 @@
id="openDialog"
class="invisible dialog"
disabled$="[[!_isValidPath(_path)]]"
- confirm-label="Open"
+ confirm-label="Confirm"
confirm-on-enter
on-confirm="_handleOpenConfirm"
on-cancel="_handleDialogCancel">
<div class="header" slot="header">
- Open an existing or new file
+ Add a new file or open an existing file
</div>
<div class="main" slot="main">
<gr-autocomplete
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
index 2027119..22ba457 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
@@ -18,6 +18,7 @@
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -37,6 +38,9 @@
#generatedPasswordDisplay {
margin: var(--spacing-l) 0;
}
+ #generatedPasswordDisplay .title {
+ width: unset;
+ }
#generatedPasswordDisplay .value {
font-family: var(--monospace-font-family);
font-size: var(--font-size-mono);
@@ -79,6 +83,12 @@
<section id="generatedPasswordDisplay">
<span class="title">New Password:</span>
<span class="value">[[_generatedPassword]]</span>
+ <gr-copy-clipboard
+ has-tooltip
+ button-title="Copy password to clipboard"
+ hide-input
+ text="[[_generatedPassword]]">
+ </gr-copy-clipboard>
</section>
<section id="passwordWarning">
This password will not be displayed again.<br>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
index ae656fd..992ea8407 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
@@ -40,7 +40,8 @@
on-commit="_handleInputCommit"
clear-on-commit
warn-uncommitted
- text="{{_inputText}}">
+ text="{{_inputText}}"
+ vertical-offset="24">
</gr-autocomplete>
</template>
<script src="gr-account-entry.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index 64950f2..cf32e28 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -60,6 +60,7 @@
.dropdown-content {
background: var(--dropdown-background-color);
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
+ border-radius: var(--border-radius);
max-height: 50vh;
overflow: auto;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index c9d12ce..e47b661 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -36,13 +36,14 @@
margin: 0 var(--spacing-xs);
vertical-align: top;
}
- paper-input:not(.borderless) {
- border: 1px solid var(--border-color);
+ paper-input.borderless {
+ border: none;
+ padding: 0;
}
paper-input {
- height: var(--line-height-normal);
- width: 100%;
- @apply --gr-autocomplete;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ padding: var(--spacing-s);
--paper-input-container: {
padding: 0;
};
@@ -50,13 +51,25 @@
font-size: var(--font-size-normal);
line-height: var(--line-height-normal);
};
+ /* This is a hack for not being able to set height:0 on the underline
+ of a paper-input 2.2.3 element. All the underline fixes below only
+ actually work in 3.x.x, so the height must be adjusted directly as
+ a workaround until we are on Polymer 3. */
+ height: var(--line-height-normal);
+ --paper-input-container-underline-height: 0;
+ --paper-input-container-underline-wrapper-height: 0;
+ --paper-input-container-underline-focus-height: 0;
+ --paper-input-container-underline-legacy-height: 0;
--paper-input-container-underline: {
+ height: 0;
display: none;
};
--paper-input-container-underline-focus: {
+ height: 0;
display: none;
};
--paper-input-container-underline-disabled: {
+ height: 0;
display: none;
};
}
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index a2c908a..3a60255 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -74,9 +74,9 @@
},
/**
- * The number of characters that must be typed before suggestions are
- * made. If threshold is zero, default suggestions are enabled.
- */
+ * The number of characters that must be typed before suggestions are
+ * made. If threshold is zero, default suggestions are enabled.
+ */
threshold: {
type: Number,
value: 1,
@@ -89,11 +89,15 @@
type: Boolean,
value: false,
},
- // Vertical offset needed for a 1em font-size with no vertical padding.
- // Inputs with additional padding will need to increase vertical offset.
+ /**
+ * Vertical offset needed for an element with 20px line-height, 4px
+ * padding and 1px border (30px height total). Plus 1px spacing between
+ * input and dropdown. Inputs with different line-height or padding will
+ * need to tweak vertical offset.
+ */
verticalOffset: {
type: Number,
- value: 20,
+ value: 31,
},
text: {
@@ -110,10 +114,10 @@
},
/**
- * When true, tab key autocompletes but does not fire the commit event.
- * When false, tab key not caught, and focus is removed from the element.
- * See Issue 4556, Issue 6645.
- */
+ * When true, tab key autocompletes but does not fire the commit event.
+ * When false, tab key not caught, and focus is removed from the element.
+ * See Issue 4556, Issue 6645.
+ */
tabComplete: {
type: Boolean,
value: false,
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 68338a4..39329e5 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -335,7 +335,7 @@
});
test('vertical offset overridden by param if it exists', () => {
- assert.equal(element.$.suggestions.verticalOffset, 20);
+ assert.equal(element.$.suggestions.verticalOffset, 31);
element.verticalOffset = 30;
assert.equal(element.$.suggestions.verticalOffset, 30);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
index 2d66cfa..0ec3d6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -50,7 +50,11 @@
return url.pathname;
}
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
- const pathname = url.pathname.replace(base, '');
+ let pathname = url.pathname.replace(base, '');
+ // Load from ASSETS_PATH
+ if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) {
+ pathname = url.href.replace(window.ASSETS_PATH, '');
+ }
// Site theme is server from predefined path.
if (pathname === '/static/gerrit-theme.html') {
return 'gerrit-theme';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
index 128738d..b43796f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -72,6 +72,15 @@
'gerrit-theme'
);
});
+
+ test('with ASSETS_PATH', () => {
+ window.ASSETS_PATH = 'http://cdn.com/2';
+ assert.equal(
+ getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
+ 'a'
+ );
+ window.ASSETS_PATH = undefined;
+ });
});
});
</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
index 4be38b6..96d8411 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -113,7 +113,7 @@
this._pluginListLoaded = true;
plugins.forEach(path => {
- const url = this._urlFor(path);
+ const url = this._urlFor(path, window.ASSETS_PATH);
// Skip if preloaded, for bundling.
if (this.isPluginPreloaded(url)) return;
@@ -128,11 +128,11 @@
});
if (this._isPathEndsWith(url, '.html')) {
- this._importHtmlPlugin(url, opts && opts[path]);
+ this._importHtmlPlugin(path, opts && opts[path]);
} else if (this._isPathEndsWith(url, '.js')) {
- this._loadJsPlugin(url);
+ this._loadJsPlugin(path);
} else {
- this._failToLoad(`Unrecognized plugin url ${url}`, url);
+ this._failToLoad(`Unrecognized plugin path ${path}`, path);
}
});
@@ -181,14 +181,15 @@
return;
}
- const pluginObject = this.getPlugin(src);
+ const url = this._urlFor(src);
+ const pluginObject = this.getPlugin(url);
let plugin = pluginObject && pluginObject.plugin;
if (!plugin) {
- plugin = new Plugin(src);
+ plugin = new Plugin(url);
}
try {
callback(plugin);
- this._pluginInstalled(src, plugin);
+ this._pluginInstalled(url, plugin);
} catch (e) {
this._failToLoad(`${e.name}: ${e.message}`, src);
}
@@ -313,38 +314,79 @@
}
_importHtmlPlugin(pluginUrl, opts = {}) {
- // onload (second param) needs to be a function. When null or undefined
- // were passed, plugins were not loaded correctly.
+ const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
+ const urlWithoutAP = this._urlFor(pluginUrl);
+ let onerror = null;
+ if (urlWithAP !== urlWithoutAP) {
+ onerror = () => this._loadHtmlPlugin(urlWithoutAP, opts.sync);
+ }
+ this._loadHtmlPlugin(urlWithAP, opts.sync, onerror);
+ }
+
+ _loadHtmlPlugin(url, sync, onerror) {
+ if (!onerror) {
+ onerror = () => {
+ this._failToLoad(`${pluginUrl} import error`, pluginUrl);
+ };
+ }
+
(Polymer.importHref || Polymer.Base.importHref)(
- this._urlFor(pluginUrl), () => {},
- () => this._failToLoad(`${pluginUrl} import error`, pluginUrl),
- !opts.sync);
+ url, () => {},
+ onerror,
+ !sync);
}
_loadJsPlugin(pluginUrl) {
- this._createScriptTag(this._urlFor(pluginUrl));
+ const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
+ const urlWithoutAP = this._urlFor(pluginUrl);
+ let onerror = null;
+ if (urlWithAP !== urlWithoutAP) {
+ onerror = () => this._createScriptTag(urlWithoutAP);
+ }
+
+ this._createScriptTag(urlWithAP, onerror);
}
- _createScriptTag(url) {
+ _createScriptTag(url, onerror) {
+ if (!onerror) {
+ onerror = () => this._failToLoad(`${url} load error`, url);
+ }
+
const el = document.createElement('script');
el.defer = true;
el.setAttribute('src', url);
- el.onerror = () => this._failToLoad(`${url} load error`, url);
+ el.onerror = onerror;
return document.body.appendChild(el);
}
- _urlFor(pathOrUrl) {
+ _urlFor(pathOrUrl, assetsPath) {
if (!pathOrUrl) {
return pathOrUrl;
}
+
+ // theme is per host, should always load from assetsPath
+ const isThemeFile = pathOrUrl.endsWith('static/gerrit-theme.html');
+ const shouldTryLoadFromAssetsPathFirst = !isThemeFile && assetsPath;
if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
pathOrUrl.startsWith('http')) {
// Plugins are loaded from another domain or preloaded.
+ if (pathOrUrl.includes(location.host)
+ && shouldTryLoadFromAssetsPathFirst) {
+ // if is loading from host server, try replace with cdn when assetsPath provided
+ return pathOrUrl
+ .replace(location.origin, assetsPath);
+ }
return pathOrUrl;
}
+
if (!pathOrUrl.startsWith('/')) {
pathOrUrl = '/' + pathOrUrl;
}
+
+ if (shouldTryLoadFromAssetsPathFirst) {
+ return assetsPath + pathOrUrl;
+ }
+
return window.location.origin + getBaseUrl() + pathOrUrl;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index 8c1ec96..08e7e18 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -325,11 +325,11 @@
let loadJsPluginStub;
setup(() => {
importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
importHtmlPluginStub(url);
});
loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
loadJsPluginStub(url);
});
});
@@ -346,8 +346,8 @@
assert.isTrue(failToLoadStub.calledOnce);
assert.isTrue(failToLoadStub.calledWithExactly(
- `Unrecognized plugin url ${url}/foo/bar`,
- `${url}/foo/bar`
+ 'Unrecognized plugin path foo/bar',
+ 'foo/bar'
));
});
@@ -407,6 +407,72 @@
});
});
+ suite('With ASSETS_PATH', () => {
+ let importHtmlPluginStub;
+ let loadJsPluginStub;
+ setup(() => {
+ window.ASSETS_PATH = 'https://cdn.com';
+ importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
+ loadJsPluginStub(url);
+ });
+ });
+
+ teardown(() => {
+ window.ASSETS_PATH = '';
+ });
+
+ test('Should try load plugins from assets path instead', () => {
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
+ });
+
+ test('Should honor original path if exists', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.html',
+ 'http://e.com/foo/bar.js',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`));
+ });
+
+ test('Should try replace current host with assetsPath', () => {
+ const host = window.location.origin;
+ Gerrit._loadPlugins([
+ `${host}/foo/bar.html`,
+ `${host}/foo/bar.js`,
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
+ });
+ });
+
test('adds js plugins will call the body', () => {
Gerrit._loadPlugins([
'http://e.com/foo/bar.js',
@@ -489,12 +555,10 @@
test('installing preloaded plugin', () => {
let plugin;
- window.ASSETS_PATH = 'http://blips.com/chitz';
Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
assert.strictEqual(plugin.getPluginName(), 'foo');
assert.strictEqual(plugin.url('/some/thing.html'),
- 'http://blips.com/chitz/plugins/foo/some/thing.html');
- delete window.ASSETS_PATH;
+ 'preloaded:foo/plugins/foo/some/thing.html');
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 6c306d9..0aaeaa1 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -17,8 +17,6 @@
(function(window) {
'use strict';
- const PRELOADED_PROTOCOL = 'preloaded:';
-
const PANEL_ENDPOINTS_MAPPING = {
CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
@@ -66,13 +64,6 @@
this._url = new URL(opt_url);
this._name = getPluginNameFromUrl(this._url);
- if (this._url.protocol === PRELOADED_PROTOCOL) {
- // Original plugin URL is used in plugin assets URLs calculation.
- const assetsBaseUrl = window.ASSETS_PATH ||
- (window.location.origin + Gerrit.BaseUrlBehavior.getBaseUrl());
- this._url = new URL(assetsBaseUrl + '/plugins/' + this._name +
- '/static/' + this._name + '.js');
- }
}
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
index da0b93f..47be6f7 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
@@ -38,14 +38,7 @@
#body {
display: flex;
}
- gr-autocomplete {
- height: 1.5em;
- --gr-autocomplete: {
- border: none;
- }
- }
#trigger {
- border-left: 1px solid var(--deemphasized-text-color);
color: var(--deemphasized-text-color);
cursor: pointer;
padding-left: var(--spacing-s);
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
index e4c23aa..fa44a47 100644
--- a/polygerrit-ui/app/samples/coverage-plugin.html
+++ b/polygerrit-ui/app/samples/coverage-plugin.html
@@ -60,6 +60,8 @@
}).enableToggleCheckbox('Display Coverage', checkbox => {
// Checkbox is attached so now add the notifier that will be controlled
// by the checkbox.
+ // Checkbox will only be added to the file diff page, in the top right
+ // section near the "Diff view".
annotationApi.addNotifier(notifyFunc => {
new Promise(resolve => setTimeout(resolve, 3000)).then(() => {
populateWithDummyData(coverageData);
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index 3fe0a72..7c9ae0d 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -61,7 +61,6 @@
}
.gr-form-styles td,
.gr-form-styles tfoot th {
- height: 2em;
padding: var(--spacing-s) 0;
vertical-align: middle;
}
@@ -84,8 +83,7 @@
.gr-form-styles textarea {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
- height: 2em;
- padding: 0 var(--spacing-xs);
+ padding: var(--spacing-s);
}
.gr-form-styles td:last-child {
width: 5em;
@@ -97,23 +95,16 @@
.gr-form-styles iron-autogrow-textarea {
border: none;
height: auto;
- min-height: 2em;
+ min-height: 4em;
--iron-autogrow-textarea: {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-sizing: border-box;
- padding: var(--spacing-s) var(--spacing-xs) 0 var(--spacing-xs);
+ padding: var(--spacing-s);
}
}
.gr-form-styles gr-autocomplete {
- border: none;
- --gr-autocomplete: {
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- height: 2em;
- padding: 0 var(--spacing-xs);
- width: 14em;
- }
+ width: 14em;
}
@media only screen and (max-width: 40em) {
.gr-form-styles section {
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 5314741..51b92e1 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -42,10 +42,11 @@
input {
background-color: inherit;
border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
box-sizing: border-box;
color: var(--primary-text-color);
margin: 0;
- padding: 0;
+ padding: var(--spacing-s);
}
iron-autogrow-textarea {
background-color: inherit;
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 35bdefd..3b699c2 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -86,6 +86,9 @@
log.Println("Local plugins from", "../plugins")
} else {
http.HandleFunc("/plugins/", handleProxy)
+ // Serve local plugins from `plugins_`
+ http.Handle("/plugins_/", http.StripPrefix("/plugins_/",
+ http.FileServer(http.Dir("../plugins"))))
}
log.Println("Serving on port", *port)
log.Fatal(http.ListenAndServe(*port, &server{}))