Merge "Add @plugins_npm for plugin dependencies"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 94c4552..ceef953 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1703,7 +1703,8 @@
[[container.slave]]container.slave::
+
-Backward compatibility for 'container.slave' config setting.
+Backward compatibility for link:#container.replica[`container.replica`]
+config setting.
[[container.startupTimeout]]container.startupTimeout::
+
@@ -2242,6 +2243,25 @@
+
By default false.
+[[gerrit.xframeOption]]gerrit.xframeOption::
++
+Add link:https://tools.ietf.org/html/rfc7034[`X-Frame-Options`] header to all HTTP
+responses. The `X-Frame-Options` HTTP response header can be used to indicate
+whether or not a browser should be allowed to render a page in a
+`<frame>`, `<iframe>`, `<embed>` or `<object>`.
++
+Available values:
++
+1. ALLOW - The page can be displayed in a frame.
+2. SAMEORIGIN - The page can only be displayed in a frame on the same origin as the page itself.
++
+If link:#gerrit.canLoadInIFrame is set to false this option is ignored and the
+`X-Frame-Options` header is always set to `DENY`.
+Setting this option to `ALLOW` will cause the `X-Frame-Options` header to be omitted
+the the page can be displayed in a frame.
++
+By default SAMEORIGIN.
+
[[gerrit.cdnPath]]gerrit.cdnPath::
+
Path prefix for PolyGerrit's static resources if using a CDN.
@@ -2976,7 +2996,7 @@
[[index.batchThreads]]index.batchThreads::
+
Number of threads to use for indexing in background operations, such as
-online schema upgrades.
+online schema upgrades, and also for offline reindexing.
+
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.
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index c298ba1..a01df50 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -316,27 +316,35 @@
The submit section includes configuration of project-specific
submit settings:
-[[content_merge]]
-- 'mergeContent': Defines whether Gerrit will try to
+[[content_merge]]submit.mergeContent::
++
+Defines whether Gerrit will try to
do a content merge when a path conflict occurs. Valid values are
'true', 'false', or 'INHERIT'. Default is 'INHERIT'. This option can
be modified by any project owner through the project console, `Browse`
> `Repositories` > my/project > `Allow content merges`.
-- 'action': Defines the link:#submit-type[submit type]. Valid
+[[submit.action]]submit.action::
++
+Defines the link:#submit-type[submit type]. Valid
values are 'fast forward only', 'merge if necessary', 'rebase if necessary',
'rebase always', 'merge always' and 'cherry pick'. The default is 'merge if necessary'.
-- 'matchAuthorToCommitterDate': Defines whether to the author date will be changed to match the
-submitter date upon submit, so that git log shows when the change was submitted instead of when the
-author last committed. Valid values are 'true', 'false', or 'INHERIT'. The default is 'INHERIT'.
-This option only takes effect in submit strategies which already modify the commit, i.e.
-Cherry Pick, Rebase Always, and (perhaps) Rebase If Necessary.
+[[submit.matchAuthorToCommitterDate]]submit.matchAuthorToCommitterDate::
++
+Defines whether the author date will be changed to match the submitter date upon submit, so that
+git log shows when the change was submitted instead of when the author last committed. Valid
+values are 'true', 'false', or 'INHERIT'. The default is 'INHERIT'. This option only takes effect
+in submit strategies which already modify the commit, i.e. Cherry Pick, Rebase Always, and
+(when rebase is necessary) Rebase If Necessary.
-- 'rejectEmptyCommit': Defines whether empty commits should be rejected when a change is merged.
-Changes might not seem empty at first but when attempting to merge, rebasing can lead to an empty
-commit. If this option is set to 'true' the merge would fail. An empty commit is still allowed as
-the initial commit on a branch.
+[[submit.rejectEmptyCommit]]submit.rejectEmptyCommit::
++
+Defines whether empty commits should be rejected when a change is merged. When using
+link:#submit.action[submit action] Cherry Pick, Rebase If Necessary or Rebase Always changes may
+become empty upon submit, since the rebase|cherry-pick can lead to an empty commit. If this option
+is set to 'true' the merge would fail in such a case. An empty commit is still allowed as the
+initial commit on a branch.
[[submit-type]]
==== Submit Type
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
index 5a54c5f..9bc7f0b 100644
--- a/Documentation/dev-crafting-changes.txt
+++ b/Documentation/dev-crafting-changes.txt
@@ -116,7 +116,7 @@
link:https://github.com/google/google-java-format[`google-java-format`,role=external,window=_blank]
tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the
link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`,role=external,window=_blank]
-tool (version 3.0.0). Unused dependencies are found and removed using the
+tool (version 3.2.1). Unused dependencies are found and removed using the
link:https://github.com/bazelbuild/buildtools/tree/master/unused_deps[`unused_deps`,role=external,window=_blank]
build tool, a sibling of `buildifier`.
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index 0505dd2..89758a0 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -110,6 +110,13 @@
normally run `gerrit.war daemon` with an `-Xmx` flag, pass that to the migration
tool as well.
+[NOTE]
+Note that by appending `--reindex false` to the above command, you can skip the
+lengthy, implicit reindexing step of the migration. This is useful if you plan
+to perform further Gerrit upgrades while the server is offline and have to
+reindex later anyway (E.g.: a follow-up upgrade to Gerrit 3.2 or newer, which
+requires to reindex changes anyway).
+
*Advantages*
* Much faster than online; can use all available CPUs, since no live traffic
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4006d1d..066f6d8 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4249,7 +4249,6 @@
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/submit HTTP/1.0
- Content-Type: application/json; charset=UTF-8
----
As response a link:#submit-info[SubmitInfo] entity is returned that
@@ -4471,7 +4470,7 @@
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_type HTTP/1.0
- Content-Type: text/plain; charset-UTF-8
+ Content-Type: text/plain; charset=UTF-8
submit_type(cherry_pick).
----
@@ -4502,7 +4501,7 @@
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_rule?filters=SKIP HTTP/1.0
- Content-Type: text/plain; charset-UTF-8
+ Content-Type: text/plain; charset=UTF-8
submit_rule(submit(R)) :-
R = label('Any-Label-Name', reject(_)).
@@ -6191,7 +6190,7 @@
Notify handling that defines to whom email notifications should be sent
after the cherry-pick. +
Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
-If not set, the default is `NONE`.
+If not set, the default is `ALL`.
|`notify_details` |optional|
Additional information about whom to notify about the update as a map
of recipient type to link:#notify-info[NotifyInfo] entity.
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 746a386..62fcfda 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -28,7 +28,8 @@
V7_4("7.4.*"),
V7_5("7.5.*"),
V7_6("7.6.*"),
- V7_7("7.7.*");
+ V7_7("7.7.*"),
+ V7_8("7.8.*");
private final String version;
private final Pattern pattern;
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index 3926d47..e6b2167 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -19,6 +19,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.UsedAt;
import java.util.List;
/**
@@ -107,6 +108,16 @@
public char getCode() {
return code;
}
+
+ @UsedAt(UsedAt.Project.COLLABNET)
+ public static ChangeType forCode(char c) {
+ for (ChangeType s : ChangeType.values()) {
+ if (s.code == c) {
+ return s;
+ }
+ }
+ return null;
+ }
}
/** Type of formatting for this patch. */
diff --git a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
index 69c1790..fb03bc5 100644
--- a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -24,7 +24,7 @@
public String base;
public Integer parent;
- public NotifyHandling notify = NotifyHandling.NONE;
+ public NotifyHandling notify = NotifyHandling.ALL;
public Map<RecipientType, NotifyInfo> notifyDetails;
public boolean keepReviewers;
diff --git a/java/com/google/gerrit/extensions/client/ListOption.java b/java/com/google/gerrit/extensions/client/ListOption.java
index 4dea42f..dba2eee 100644
--- a/java/com/google/gerrit/extensions/client/ListOption.java
+++ b/java/com/google/gerrit/extensions/client/ListOption.java
@@ -48,9 +48,9 @@
return r;
}
- static String toHex(Set<ListChangesOption> options) {
+ static <T extends Enum<T> & ListOption> String toHex(Set<T> options) {
int v = 0;
- for (ListChangesOption option : options) {
+ for (T option : options) {
v |= 1 << option.getValue();
}
diff --git a/java/com/google/gerrit/httpd/AllRequestFilter.java b/java/com/google/gerrit/httpd/AllRequestFilter.java
index 9d171d5..1c3cafe 100644
--- a/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -18,6 +18,8 @@
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.StopPluginListener;
import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
@@ -32,11 +34,15 @@
/** Filters all HTTP requests passing through the server. */
public abstract class AllRequestFilter implements Filter {
- public static ServletModule module() {
+ public static Module module() {
return new ServletModule() {
@Override
protected void configureServlets() {
DynamicSet.setOf(binder(), AllRequestFilter.class);
+ DynamicSet.bind(binder(), AllRequestFilter.class)
+ .to(AllowRenderInFrameFilter.class)
+ .in(Scopes.SINGLETON);
+
filter("/*").through(FilterProxy.class);
bind(StopPluginListener.class)
diff --git a/java/com/google/gerrit/httpd/AllowRenderInFrameFilter.java b/java/com/google/gerrit/httpd/AllowRenderInFrameFilter.java
new file mode 100644
index 0000000..b414aad
--- /dev/null
+++ b/java/com/google/gerrit/httpd/AllowRenderInFrameFilter.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2020 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.httpd;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
+
+public class AllowRenderInFrameFilter extends AllRequestFilter {
+ static final String X_FRAME_OPTIONS_HEADER_NAME = "X-Frame-Options";
+
+ public static enum XFrameOption {
+ ALLOW,
+ SAMEORIGIN;
+ }
+
+ private final String xframeOptionString;
+ private final boolean skipXFrameOption;
+
+ @Inject
+ public AllowRenderInFrameFilter(@GerritServerConfig Config cfg) {
+ XFrameOption xframeOption =
+ cfg.getEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
+ boolean canLoadInIFrame = cfg.getBoolean("gerrit", "canLoadInIFrame", false);
+ xframeOptionString = canLoadInIFrame ? xframeOption.name() : "DENY";
+
+ skipXFrameOption = xframeOption.equals(XFrameOption.ALLOW) && canLoadInIFrame;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ if (skipXFrameOption) {
+ chain.doFilter(request, response);
+ } else {
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ httpResponse.addHeader(X_FRAME_OPTIONS_HEADER_NAME, xframeOptionString);
+ chain.doFilter(request, httpResponse);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 41d2f83..cddaea4 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -153,8 +153,7 @@
serializeObject(GSON, accountApi.getEditPreferences()));
data.put("userIsAuthenticated", true);
} catch (AuthException e) {
- logger.atFine().withCause(e).log(
- "Can't inline account-related data because user is unauthenticated");
+ logger.atFine().log("Can't inline account-related data because user is unauthenticated");
// Don't render data
}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 89ebdc1..f743578 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -601,6 +601,7 @@
}
} catch (MalformedJsonException | JsonParseException e) {
cause = Optional.of(e);
+ logger.atFine().withCause(e).log("REST call failed on JSON parsing");
responseBytes =
replyError(
req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 0aa374b..3aa9de0 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -176,6 +177,33 @@
return true;
}
+ private Values<T> fieldValues(T obj, FieldDef<T, ?> f, ImmutableSet<String> skipFields) {
+ if (skipFields.contains(f.getName())) {
+ return null;
+ }
+
+ Object v;
+ try {
+ v = f.get(obj);
+ } catch (StorageException e) {
+ // StorageException is thrown when the object is not found. On this case,
+ // it is pointless to make further attempts for each field, so propagate
+ // the exception to return an empty list.
+ logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
+ throw e;
+ } catch (RuntimeException e) {
+ logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
+ return null;
+ }
+ if (v == null) {
+ return null;
+ } else if (f.isRepeatable()) {
+ return new Values<>(f, (Iterable<?>) v);
+ } else {
+ return new Values<>(f, Collections.singleton(v));
+ }
+ }
+
/**
* Build all fields in the schema from an input object.
*
@@ -186,31 +214,14 @@
* @return all non-null field values from the object.
*/
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);
- } catch (RuntimeException e) {
- logger.atSevere().withCause(e).log(
- "error getting field %s of %s", f.getName(), obj);
- return null;
- }
- if (v == null) {
- return null;
- } else if (f.isRepeatable()) {
- return new Values<>(f, (Iterable<?>) v);
- } else {
- return new Values<>(f, Collections.singleton(v));
- }
- })
- .filter(Objects::nonNull)
- .collect(toImmutableList());
+ try {
+ return fields.values().stream()
+ .map(f -> fieldValues(obj, f, skipFields))
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
+ } catch (StorageException e) {
+ return ImmutableList.of();
+ }
}
@Override
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index ecfc7bd..32b4b21 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -83,7 +83,7 @@
}
protected PrintWriter newPrintWriter(OutputStream out) {
- return new PrintWriter(new OutputStreamWriter(out, UTF_8));
+ return new PrintWriter(new OutputStreamWriter(out, UTF_8), true);
}
private static class ErrorListener implements Runnable {
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 2e526bb..966801f 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -202,6 +202,9 @@
if (result.success()) {
index.markReady(true);
}
+ System.out.format(
+ "Index %s in version %d is %sready\n",
+ def.getName(), index.getSchema().getVersion(), result.success() ? "" : "NOT ");
return result.success();
}
}
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 2a8a5ba..e9349c4 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -21,7 +21,6 @@
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import com.google.common.base.Stopwatch;
-import com.google.common.collect.ComparisonChain;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.ListenableFuture;
@@ -43,10 +42,9 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
-import java.util.SortedSet;
-import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -62,6 +60,7 @@
*/
public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final int PROJECT_SLICE_MAX_REFS = 1000;
private final ChangeData.Factory changeDataFactory;
private final GitRepositoryManager repoManager;
@@ -86,22 +85,27 @@
this.projectCache = projectCache;
}
- private static class ProjectHolder implements Comparable<ProjectHolder> {
- final Project.NameKey name;
- private final long size;
+ private static class ProjectSlice {
+ private final Project.NameKey name;
+ private final int slice;
+ private final int slices;
- ProjectHolder(Project.NameKey name, long size) {
+ ProjectSlice(Project.NameKey name, int slice, int slices) {
this.name = name;
- this.size = size;
+ this.slice = slice;
+ this.slices = slices;
}
- @Override
- public int compareTo(ProjectHolder other) {
- // Sort projects based on size first to maximize utilization of threads early on.
- return ComparisonChain.start()
- .compare(other.size, size)
- .compare(other.name.get(), name.get())
- .result();
+ public Project.NameKey getName() {
+ return name;
+ }
+
+ public int getSlice() {
+ return slice;
+ }
+
+ public int getSlices() {
+ return slices;
}
}
@@ -109,19 +113,39 @@
public Result indexAll(ChangeIndex index) {
ProgressMonitor pm = new TextProgressMonitor();
pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
- SortedSet<ProjectHolder> projects = new TreeSet<>();
+ List<ProjectSlice> projectSlices = new ArrayList<>();
int changeCount = 0;
Stopwatch sw = Stopwatch.createStarted();
int projectsFailed = 0;
for (Project.NameKey name : projectCache.all()) {
try (Repository repo = repoManager.openRepository(name)) {
+ // The simplest approach to distribute indexing would be to let each thread grab a project
+ // and index it fully. But if a site has one big project and 100s of small projects, then
+ // in the beginning all CPUs would be busy reindexing projects. But soon enough all small
+ // projects have been reindexed, and only the thread that reindexes the big project is
+ // still working. The other threads would idle. Reindexing the big project on a single
+ // thread becomes the critical path. Bringing in more CPUs would not speed up things.
+ //
+ // To avoid such situations, we split big repos into smaller parts and let
+ // the thread pool index these smaller parts. This splitting introduces an overhead in the
+ // workload setup and there might be additional slow-downs from multiple threads
+ // concurrently working on different parts of the same project. But for Wikimedia's Gerrit,
+ // which had 2 big projects, many middle sized ones, and lots of smaller ones, the
+ // splitting of repos into smaller parts reduced indexing time from 1.5 hours to 55 minutes
+ // in 2020.
int size = estimateSize(repo);
changeCount += size;
- projects.add(new ProjectHolder(name, size));
+ int slices = 1 + size / PROJECT_SLICE_MAX_REFS;
+ if (slices > 1) {
+ verboseWriter.println("Submitting " + name + " for indexing in " + slices + " slices");
+ }
+ for (int slice = 0; slice < slices; slice++) {
+ projectSlices.add(new ProjectSlice(name, slice, slices));
+ }
} catch (IOException e) {
logger.atSevere().withCause(e).log("Error collecting project %s", name);
projectsFailed++;
- if (projectsFailed > projects.size() / 2) {
+ if (projectsFailed > projectCache.all().size() / 2) {
logger.atSevere().log("Over 50%% of the projects could not be collected: aborted");
return Result.create(sw, false, 0, 0);
}
@@ -130,7 +154,15 @@
}
pm.endTask();
setTotalWork(changeCount);
- return indexAll(index, projects);
+
+ // projectSlices are currently grouped by projects. First all slices for project1, followed
+ // by all slices for project2, and so on. As workers pick tasks sequentially, multiple threads
+ // would typically work concurrently on different slices of the same project. While this is not
+ // a big issue, shuffling the list beforehand helps with ungrouping the project slices, so
+ // different slices are less likely to be worked on concurrently.
+ // This shuffling gave a 6% runtime reduction for Wikimedia's Gerrit in 2020.
+ Collections.shuffle(projectSlices);
+ return indexAll(index, projectSlices);
}
private int estimateSize(Repository repo) throws IOException {
@@ -146,10 +178,10 @@
return Ints.saturatedCast(size);
}
- private SiteIndexer.Result indexAll(ChangeIndex index, SortedSet<ProjectHolder> projects) {
+ private SiteIndexer.Result indexAll(ChangeIndex index, List<ProjectSlice> projectSlices) {
Stopwatch sw = Stopwatch.createStarted();
MultiProgressMonitor mpm = new MultiProgressMonitor(progressOut, "Reindexing changes");
- Task projTask = mpm.beginSubTask("projects", projects.size());
+ Task projTask = mpm.beginSubTask("project-slices", projectSlices.size());
checkState(totalWork >= 0);
Task doneTask = mpm.beginSubTask(null, totalWork);
Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
@@ -157,12 +189,21 @@
List<ListenableFuture<?>> futures = new ArrayList<>();
AtomicBoolean ok = new AtomicBoolean(true);
- for (ProjectHolder project : projects) {
+ for (ProjectSlice projectSlice : projectSlices) {
+ Project.NameKey name = projectSlice.getName();
+ int slice = projectSlice.getSlice();
+ int slices = projectSlice.getSlices();
ListenableFuture<?> future =
executor.submit(
reindexProject(
- indexerFactory.create(executor, index), project.name, doneTask, failedTask));
- addErrorListener(future, "project " + project.name, projTask, ok);
+ indexerFactory.create(executor, index),
+ name,
+ slice,
+ slices,
+ doneTask,
+ failedTask));
+ String description = "project " + name + " (" + slice + "/" + slices + ")";
+ addErrorListener(future, description, projTask, ok);
futures.add(future);
}
@@ -197,22 +238,38 @@
public Callable<Void> reindexProject(
ChangeIndexer indexer, Project.NameKey project, Task done, Task failed) {
- return new ProjectIndexer(indexer, project, done, failed);
+ return reindexProject(indexer, project, 0, 1, done, failed);
+ }
+
+ public Callable<Void> reindexProject(
+ ChangeIndexer indexer,
+ Project.NameKey project,
+ int slice,
+ int slices,
+ Task done,
+ Task failed) {
+ return new ProjectIndexer(indexer, project, slice, slices, done, failed);
}
private class ProjectIndexer implements Callable<Void> {
private final ChangeIndexer indexer;
private final Project.NameKey project;
+ private final int slice;
+ private final int slices;
private final ProgressMonitor done;
private final ProgressMonitor failed;
private ProjectIndexer(
ChangeIndexer indexer,
Project.NameKey project,
+ int slice,
+ int slices,
ProgressMonitor done,
ProgressMonitor failed) {
this.indexer = indexer;
this.project = project;
+ this.slice = slice;
+ this.slices = slices;
this.done = done;
this.failed = failed;
}
@@ -227,7 +284,7 @@
// It does mean that reindexing after invalidating the DiffSummary cache will be expensive,
// but the goal is to invalidate that cache as infrequently as we possibly can. And besides,
// we don't have concrete proof that improving packfile locality would help.
- notesFactory.scan(repo, project).forEach(r -> index(r));
+ notesFactory.scan(repo, project, id -> (id.get() % slices) == slice).forEach(r -> index(r));
} catch (RepositoryNotFoundException rnfe) {
logger.atSevere().log(rnfe.getMessage());
} finally {
@@ -244,7 +301,8 @@
try {
indexer.index(changeDataFactory.create(r.notes()));
done.update(1);
- verboseWriter.println("Reindexed change " + r.id());
+ verboseWriter.format(
+ "Reindexed change %d (project: %s)\n", r.id().get(), r.notes().getProjectName().get());
} catch (RejectedExecutionException e) {
// Server shutdown, don't spam the logs.
failSilently();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 0a71fa1..36a61cc0 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -207,9 +207,19 @@
public Stream<ChangeNotesResult> scan(Repository repo, Project.NameKey project)
throws IOException {
+ return scan(repo, project, null);
+ }
+
+ public Stream<ChangeNotesResult> scan(
+ Repository repo, Project.NameKey project, Predicate<Change.Id> changeIdPredicate)
+ throws IOException {
ScanResult sr = scanChangeIds(repo);
- return sr.all().stream().map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
+ Stream<Change.Id> idStream = sr.all().stream();
+ if (changeIdPredicate != null) {
+ idStream = idStream.filter(changeIdPredicate);
+ }
+ return idStream.map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
}
@Nullable
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index f2254d6..f00df53 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -18,6 +18,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.entities.Project;
@@ -34,6 +35,7 @@
/** Collection of routines to populate {@link ProjectInfo}. */
@Singleton
public class ProjectJson {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final AllProjectsName allProjects;
private final WebLinks webLinks;
@@ -50,7 +52,17 @@
for (LabelType t : projectState.getLabelTypes().getLabelTypes()) {
LabelTypeInfo labelInfo = new LabelTypeInfo();
labelInfo.values =
- t.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText));
+ t.getValues().stream()
+ .collect(
+ toMap(
+ LabelValue::formatValue,
+ LabelValue::getText,
+ (v1, v2) -> {
+ logger.atSevere().log(
+ "Duplicate values for project: %s, label: %s found: '%s':'%s'",
+ projectState.getName(), t.getName(), v1, v2);
+ return v1;
+ }));
labelInfo.defaultValue = t.getDefaultValue();
info.labels.put(t.getName(), labelInfo);
}
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 534c164..e77bfe7 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -214,7 +214,7 @@
updatedChange = op.merge(change, submitter, true, input, false);
if (updatedChange.isMerged()) {
- return Response.ok(new Output(change));
+ return Response.ok(new Output(updatedChange));
}
throw new IllegalStateException(
diff --git a/java/com/google/gerrit/server/rules/PrologOptions.java b/java/com/google/gerrit/server/rules/PrologOptions.java
index da9b3ab..a176f04 100644
--- a/java/com/google/gerrit/server/rules/PrologOptions.java
+++ b/java/com/google/gerrit/server/rules/PrologOptions.java
@@ -15,18 +15,21 @@
package com.google.gerrit.server.rules;
import com.google.auto.value.AutoValue;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import java.util.Optional;
@AutoValue
public abstract class PrologOptions {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static PrologOptions defaultOptions() {
return new AutoValue_PrologOptions.Builder().logErrors(true).skipFilters(false).build();
}
public static PrologOptions dryRunOptions(String ruleToTest, boolean skipFilters) {
return new AutoValue_PrologOptions.Builder()
- .logErrors(false)
+ .logErrors(logger.atFine().isEnabled())
.skipFilters(skipFilters)
.rule(ruleToTest)
.build();
diff --git a/java/com/google/gerrit/server/submit/CircularPathFinder.java b/java/com/google/gerrit/server/submit/CircularPathFinder.java
new file mode 100644
index 0000000..d1920da
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/CircularPathFinder.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2020 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.submit;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+class CircularPathFinder {
+ private CircularPathFinder() {}
+
+ /**
+ * Prints a circular path according to the nodes in {@code p} and the start node {@code target}.
+ */
+ public static <T> String printCircularPath(Collection<T> p, T target) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(target);
+ ArrayList<T> reverseP = new ArrayList<>(p);
+ Collections.reverse(reverseP);
+ for (T t : reverseP) {
+ sb.append("->");
+ sb.append(t);
+ if (t.equals(target)) {
+ break;
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 9018d9d..f96b0c5 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -635,13 +635,13 @@
throw e;
}
- // BatchUpdate may have inadvertently wrapped an IntegrationException
+ // BatchUpdate may have inadvertently wrapped an IntegrationConflictException
// thrown by some legacy SubmitStrategyOp code that intended the error
// message to be user-visible. Copy the message from the wrapped
// exception.
//
// If you happen across one of these, the correct fix is to convert the
- // inner IntegrationException to a ResourceConflictException.
+ // inner IntegrationConflictException to a ResourceConflictException.
if (e.getCause() instanceof IntegrationConflictException) {
throw (IntegrationConflictException) e.getCause();
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 5558c74..ab28aa9 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -23,7 +23,6 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -36,13 +35,11 @@
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectState;
@@ -394,24 +391,13 @@
}
}
- private String getByAccountName() {
- requireNonNull(submitter, "getByAccountName called before submitter populated");
- Optional<Account> account =
- args.accountCache.get(submitter.accountId()).map(AccountState::account);
- if (account.isPresent() && account.get().fullName() != null) {
- return " by " + ChangeNoteUtil.getAccountIdAsUsername(account.get().id());
- }
- return "";
- }
-
private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit, CommitMergeStatus s) {
requireNonNull(s, "CommitMergeStatus may not be null");
String txt = s.getDescription();
if (s == CommitMergeStatus.CLEAN_MERGE) {
- return message(ctx, commit.getPatchsetId(), txt + getByAccountName());
+ return message(ctx, commit.getPatchsetId(), txt);
} else if (s == CommitMergeStatus.CLEAN_REBASE || s == CommitMergeStatus.CLEAN_PICK) {
- return message(
- ctx, commit.getPatchsetId(), txt + " as " + commit.name() + getByAccountName());
+ return message(ctx, commit.getPatchsetId(), txt + " as " + commit.name());
} else if (s == CommitMergeStatus.SKIPPED_IDENTICAL_TREE) {
return message(ctx, commit.getPatchsetId(), txt);
} else if (s == CommitMergeStatus.ALREADY_MERGED) {
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index 6854e90..5adda2c 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -14,19 +14,14 @@
package com.google.gerrit.server.submit;
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.SetMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.common.data.SubscribeSection;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmoduleSubscription;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -46,17 +41,11 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -72,10 +61,8 @@
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.RefSpec;
public class SubmoduleOp {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -126,40 +113,15 @@
}
}
- private final GitModules.Factory gitmodulesFactory;
private final PersonIdent myIdent;
- private final ProjectCache projectCache;
private final VerboseSuperprojectUpdate verboseSuperProject;
- private final boolean enableSuperProjectSubscriptions;
private final long maxCombinedCommitMessageSize;
private final long maxCommitMessages;
private final MergeOpRepoManager orm;
- private final Map<BranchNameKey, GitModules> branchGitModules;
-
- /** Branches updated as part of the enclosing submit or push batch. */
- private final ImmutableSet<BranchNameKey> updatedBranches;
-
- /**
- * Branches in a superproject that contain submodule subscriptions, plus branches in submodules
- * which are subscribed to by some superproject.
- */
- private final Set<BranchNameKey> affectedBranches;
-
- /** Copy of {@link #affectedBranches}, sorted by submodule traversal order. */
- private final ImmutableSet<BranchNameKey> sortedBranches;
-
- /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
- private final SetMultimap<BranchNameKey, SubmoduleSubscription> targets;
+ private final SubscriptionGraph.Factory subscriptionGraphFactory;
+ private final SubscriptionGraph subscriptionGraph;
private final BranchTips branchTips = new BranchTips();
- /**
- * Multimap of superproject name to all branch names within that superproject which have submodule
- * subscriptions.
- */
- private final SetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
-
- /** All branches subscribed by other projects. */
- private final Set<BranchNameKey> subscribedBranches;
private SubmoduleOp(
GitModules.Factory gitmodulesFactory,
@@ -169,239 +131,27 @@
Set<BranchNameKey> updatedBranches,
MergeOpRepoManager orm)
throws SubmoduleConflictException {
- this.gitmodulesFactory = gitmodulesFactory;
this.myIdent = myIdent;
- this.projectCache = projectCache;
this.verboseSuperProject =
cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
- this.enableSuperProjectSubscriptions =
- cfg.getBoolean("submodule", "enableSuperProjectSubscriptions", true);
this.maxCombinedCommitMessageSize =
cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
this.orm = orm;
- this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
- this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
- this.affectedBranches = new HashSet<>();
- this.branchGitModules = new HashMap<>();
- this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build();
- this.subscribedBranches = new HashSet<>();
- this.sortedBranches = calculateSubscriptionMaps();
- }
-
- /**
- * Calculate the internal maps used by the operation.
- *
- * <p>In addition to the return value, the following fields are populated as a side effect:
- *
- * <ul>
- * <li>{@link #affectedBranches}
- * <li>{@link #targets}
- * <li>{@link #branchesByProject}
- * <li>{@link #subscribedBranches}
- * </ul>
- *
- * @return the ordered set to be stored in {@link #sortedBranches}.
- */
- // TODO(dborowitz): This setup process is hard to follow, in large part due to the accumulation of
- // mutable maps, which makes this whole class difficult to understand.
- //
- // A cleaner architecture for this process might be:
- // 1. Separate out the code to parse submodule subscriptions and build up an in-memory data
- // structure representing the subscription graph, using a separate class with a properly-
- // documented interface.
- // 2. Walk the graph to produce a work plan. This would be a list of items indicating: create a
- // commit in project X reading branch tips for submodules S1..Sn and updating gitlinks in X.
- // 3. Execute the work plan, i.e. convert the items into BatchUpdate.Ops and add them to the
- // relevant updates.
- //
- // In addition to improving readability, this approach has the advantage of making (1) and (2)
- // testable using small tests.
- private ImmutableSet<BranchNameKey> calculateSubscriptionMaps()
- throws SubmoduleConflictException {
- if (!enableSuperProjectSubscriptions) {
+ this.subscriptionGraphFactory =
+ new SubscriptionGraph.DefaultFactory(gitmodulesFactory, projectCache, orm);
+ if (cfg.getBoolean("submodule", "enableSuperProjectSubscriptions", true)) {
+ this.subscriptionGraph = subscriptionGraphFactory.compute(updatedBranches);
+ } else {
logger.atFine().log("Updating superprojects disabled");
- return null;
+ this.subscriptionGraph =
+ SubscriptionGraph.createEmptyGraph(ImmutableSet.copyOf(updatedBranches));
}
-
- logger.atFine().log("Calculating superprojects - submodules map");
- LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>();
- for (BranchNameKey updatedBranch : updatedBranches) {
- if (allVisited.contains(updatedBranch)) {
- continue;
- }
-
- searchForSuperprojects(updatedBranch, new LinkedHashSet<>(), allVisited);
- }
-
- // Since the searchForSuperprojects will add all branches (related or
- // unrelated) and ensure the superproject's branches get added first before
- // a submodule branch. Need remove all unrelated branches and reverse
- // the order.
- allVisited.retainAll(affectedBranches);
- reverse(allVisited);
- return ImmutableSet.copyOf(allVisited);
- }
-
- private void searchForSuperprojects(
- BranchNameKey current,
- LinkedHashSet<BranchNameKey> currentVisited,
- LinkedHashSet<BranchNameKey> allVisited)
- throws SubmoduleConflictException {
- logger.atFine().log("Now processing %s", current);
-
- if (currentVisited.contains(current)) {
- throw new SubmoduleConflictException(
- "Branch level circular subscriptions detected: "
- + printCircularPath(currentVisited, current));
- }
-
- if (allVisited.contains(current)) {
- return;
- }
-
- currentVisited.add(current);
- try {
- Collection<SubmoduleSubscription> subscriptions =
- superProjectSubscriptionsForSubmoduleBranch(current);
- for (SubmoduleSubscription sub : subscriptions) {
- BranchNameKey superBranch = sub.getSuperProject();
- searchForSuperprojects(superBranch, currentVisited, allVisited);
- targets.put(superBranch, sub);
- branchesByProject.put(superBranch.project(), superBranch);
- affectedBranches.add(superBranch);
- affectedBranches.add(sub.getSubmodule());
- subscribedBranches.add(sub.getSubmodule());
- }
- } catch (IOException e) {
- throw new StorageException("Cannot find superprojects for " + current, e);
- }
- currentVisited.remove(current);
- allVisited.add(current);
- }
-
- private static <T> void reverse(LinkedHashSet<T> set) {
- if (set == null) {
- return;
- }
-
- Deque<T> q = new ArrayDeque<>(set);
- set.clear();
-
- while (!q.isEmpty()) {
- set.add(q.removeLast());
- }
- }
-
- private <T> String printCircularPath(LinkedHashSet<T> p, T target) {
- StringBuilder sb = new StringBuilder();
- sb.append(target);
- ArrayList<T> reverseP = new ArrayList<>(p);
- Collections.reverse(reverseP);
- for (T t : reverseP) {
- sb.append("->");
- sb.append(t);
- if (t.equals(target)) {
- break;
- }
- }
- return sb.toString();
- }
-
- private Collection<BranchNameKey> getDestinationBranches(BranchNameKey src, SubscribeSection s)
- throws IOException {
- Collection<BranchNameKey> ret = new HashSet<>();
- logger.atFine().log("Inspecting SubscribeSection %s", s);
- for (RefSpec r : s.getMatchingRefSpecs()) {
- logger.atFine().log("Inspecting [matching] ref %s", r);
- if (!r.matchSource(src.branch())) {
- continue;
- }
- if (r.isWildcard()) {
- // refs/heads/*[:refs/somewhere/*]
- ret.add(
- BranchNameKey.create(
- s.getProject(), r.expandFromSource(src.branch()).getDestination()));
- } else {
- // e.g. refs/heads/master[:refs/heads/stable]
- String dest = r.getDestination();
- if (dest == null) {
- dest = r.getSource();
- }
- ret.add(BranchNameKey.create(s.getProject(), dest));
- }
- }
-
- for (RefSpec r : s.getMultiMatchRefSpecs()) {
- logger.atFine().log("Inspecting [all] ref %s", r);
- if (!r.matchSource(src.branch())) {
- continue;
- }
- OpenRepo or;
- try {
- or = orm.getRepo(s.getProject());
- } catch (NoSuchProjectException e) {
- // A project listed a non existent project to be allowed
- // to subscribe to it. Allow this for now, i.e. no exception is
- // thrown.
- continue;
- }
-
- for (Ref ref : or.repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS)) {
- if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
- continue;
- }
- BranchNameKey b = BranchNameKey.create(s.getProject(), ref.getName());
- if (!ret.contains(b)) {
- ret.add(b);
- }
- }
- }
- logger.atFine().log("Returning possible branches: %s for project %s", ret, s.getProject());
- return ret;
- }
-
- private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
- BranchNameKey srcBranch) throws IOException {
- logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
- Collection<SubmoduleSubscription> ret = new ArrayList<>();
- Project.NameKey srcProject = srcBranch.project();
- for (SubscribeSection s :
- projectCache
- .get(srcProject)
- .orElseThrow(illegalState(srcProject))
- .getSubscribeSections(srcBranch)) {
- logger.atFine().log("Checking subscribe section %s", s);
- Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s);
- for (BranchNameKey targetBranch : branches) {
- Project.NameKey targetProject = targetBranch.project();
- try {
- OpenRepo or = orm.getRepo(targetProject);
- ObjectId id = or.repo.resolve(targetBranch.branch());
- if (id == null) {
- logger.atFine().log("The branch %s doesn't exist.", targetBranch);
- continue;
- }
- } catch (NoSuchProjectException e) {
- logger.atFine().log("The project %s doesn't exist", targetProject);
- continue;
- }
-
- GitModules m = branchGitModules.get(targetBranch);
- if (m == null) {
- m = gitmodulesFactory.create(targetBranch, orm);
- branchGitModules.put(targetBranch, m);
- }
- ret.addAll(m.subscribedTo(srcBranch));
- }
- }
- logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret);
- return ret;
}
@UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
public boolean hasSuperproject(BranchNameKey branch) {
- return subscribedBranches.contains(branch);
+ return subscriptionGraph.hasSuperproject(branch);
}
public void updateSuperProjects() throws RestApiException {
@@ -414,11 +164,11 @@
try {
for (Project.NameKey project : projects) {
// only need superprojects
- if (branchesByProject.containsKey(project)) {
+ if (subscriptionGraph.isAffectedSuperProject(project)) {
superProjects.add(project);
// get a new BatchUpdate for the super project
OpenRepo or = orm.getRepo(project);
- for (BranchNameKey branch : branchesByProject.get(project)) {
+ for (BranchNameKey branch : subscriptionGraph.getAffectedSuperBranches(project)) {
addOp(or.getUpdate(), branch);
}
}
@@ -454,7 +204,7 @@
int count = 0;
List<SubmoduleSubscription> subscriptions =
- targets.get(subscriber).stream()
+ subscriptionGraph.getSubscriptions(subscriber).stream()
.sorted(comparing(SubmoduleSubscription::getPath))
.collect(toList());
for (SubmoduleSubscription s : subscriptions) {
@@ -507,7 +257,7 @@
StringBuilder msgbuf = new StringBuilder();
DirCache dc = readTree(or.rw, currentCommit);
DirCacheEditor ed = dc.editor();
- for (SubmoduleSubscription s : targets.get(subscriber)) {
+ for (SubmoduleSubscription s : subscriptionGraph.getSubscriptions(subscriber)) {
updateSubmodule(dc, ed, msgbuf, s);
}
ed.finish();
@@ -584,6 +334,7 @@
}
CodeReviewCommit newCommit = maybeNewCommit.get();
+
if (Objects.equals(newCommit, oldCommit)) {
// gitlink have already been updated for this submodule
return null;
@@ -669,11 +420,11 @@
ImmutableSet<Project.NameKey> getProjectsInOrder() throws SubmoduleConflictException {
LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>();
- for (Project.NameKey project : branchesByProject.keySet()) {
+ for (Project.NameKey project : subscriptionGraph.getAffectedSuperProjects()) {
addAllSubmoduleProjects(project, new LinkedHashSet<>(), projects);
}
- for (BranchNameKey branch : updatedBranches) {
+ for (BranchNameKey branch : subscriptionGraph.getUpdatedBranches()) {
projects.add(branch.project());
}
return ImmutableSet.copyOf(projects);
@@ -686,7 +437,8 @@
throws SubmoduleConflictException {
if (current.contains(project)) {
throw new SubmoduleConflictException(
- "Project level circular subscriptions detected: " + printCircularPath(current, project));
+ "Project level circular subscriptions detected: "
+ + CircularPathFinder.printCircularPath(current, project));
}
if (projects.contains(project)) {
@@ -695,8 +447,8 @@
current.add(project);
Set<Project.NameKey> subprojects = new HashSet<>();
- for (BranchNameKey branch : branchesByProject.get(project)) {
- Collection<SubmoduleSubscription> subscriptions = targets.get(branch);
+ for (BranchNameKey branch : subscriptionGraph.getAffectedSuperBranches(project)) {
+ Collection<SubmoduleSubscription> subscriptions = subscriptionGraph.getSubscriptions(branch);
for (SubmoduleSubscription s : subscriptions) {
subprojects.add(s.getSubmodule().project());
}
@@ -712,15 +464,13 @@
ImmutableSet<BranchNameKey> getBranchesInOrder() {
LinkedHashSet<BranchNameKey> branches = new LinkedHashSet<>();
- if (sortedBranches != null) {
- branches.addAll(sortedBranches);
- }
- branches.addAll(updatedBranches);
+ branches.addAll(subscriptionGraph.getSortedSuperprojectAndSubmoduleBranches());
+ branches.addAll(subscriptionGraph.getUpdatedBranches());
return ImmutableSet.copyOf(branches);
}
boolean hasSubscription(BranchNameKey branch) {
- return targets.containsKey(branch);
+ return subscriptionGraph.hasSubscription(branch);
}
void addBranchTip(BranchNameKey branch, CodeReviewCommit tip) {
diff --git a/java/com/google/gerrit/server/submit/SubscriptionGraph.java b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
new file mode 100644
index 0000000..406d878
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
@@ -0,0 +1,375 @@
+// Copyright (C) 2020 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.submit;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmoduleSubscription;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.RefSpec;
+
+/**
+ * A container which stores subscription relationship. A SubscriptionGraph is calculated every time
+ * changes are pushed. Some branches are updated in these changes, and if these branches are
+ * subscribed by other projects, SubscriptionGraph would record information about these updated
+ * branches and branches/projects affected.
+ */
+public class SubscriptionGraph {
+ /** Branches updated as part of the enclosing submit or push batch. */
+ private final ImmutableSet<BranchNameKey> updatedBranches;
+
+ /**
+ * All branches affected, including those in superprojects and submodules, sorted by submodule
+ * traversal order. To support nested subscriptions, GitLink commits need to be updated in order.
+ * The closer to topological "leaf", the earlier a commit should be updated.
+ *
+ * <p>For example, there are three projects, top level project p1 subscribed to p2, p2 subscribed
+ * to bottom level project p3. When submit a change for p3. We need update both p2 and p1. To be
+ * more precise, we need update p2 first and then update p1.
+ */
+ private final ImmutableSet<BranchNameKey> sortedBranches;
+
+ /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
+ private final ImmutableSetMultimap<BranchNameKey, SubmoduleSubscription> targets;
+
+ /**
+ * Multimap of superproject name to all branch names within that superproject which have submodule
+ * subscriptions.
+ */
+ private final ImmutableSetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
+
+ /** All branches subscribed by other projects. */
+ private final ImmutableSet<BranchNameKey> subscribedBranches;
+
+ public SubscriptionGraph(
+ Set<BranchNameKey> updatedBranches,
+ SetMultimap<BranchNameKey, SubmoduleSubscription> targets,
+ SetMultimap<Project.NameKey, BranchNameKey> branchesByProject,
+ Set<BranchNameKey> subscribedBranches,
+ Set<BranchNameKey> sortedBranches) {
+ this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
+ this.targets = ImmutableSetMultimap.copyOf(targets);
+ this.branchesByProject = ImmutableSetMultimap.copyOf(branchesByProject);
+ this.subscribedBranches = ImmutableSet.copyOf(subscribedBranches);
+ this.sortedBranches = ImmutableSet.copyOf(sortedBranches);
+ }
+
+ /** Returns an empty {@code SubscriptionGraph}. */
+ static SubscriptionGraph createEmptyGraph(Set<BranchNameKey> updatedBranches) {
+ return new SubscriptionGraph(
+ updatedBranches,
+ ImmutableSetMultimap.of(),
+ ImmutableSetMultimap.of(),
+ ImmutableSet.of(),
+ ImmutableSet.of());
+ }
+
+ /** Get branches updated as part of the enclosing submit or push batch. */
+ ImmutableSet<BranchNameKey> getUpdatedBranches() {
+ return updatedBranches;
+ }
+
+ /** Get all superprojects affected. */
+ ImmutableSet<Project.NameKey> getAffectedSuperProjects() {
+ return branchesByProject.keySet();
+ }
+
+ /** See if a {@code project} is a superproject affected. */
+ boolean isAffectedSuperProject(Project.NameKey project) {
+ return branchesByProject.containsKey(project);
+ }
+
+ /**
+ * Returns all branches within the superproject {@code project} which have submodule
+ * subscriptions.
+ */
+ ImmutableSet<BranchNameKey> getAffectedSuperBranches(Project.NameKey project) {
+ return branchesByProject.get(project);
+ }
+
+ /**
+ * Get all affected branches, including the submodule branches and superproject branches, sorted
+ * by traversal order.
+ *
+ * @see SubscriptionGraph#sortedBranches
+ */
+ ImmutableSet<BranchNameKey> getSortedSuperprojectAndSubmoduleBranches() {
+ return sortedBranches;
+ }
+
+ /** Check if a {@code branch} is a submodule of a superproject. */
+ boolean hasSuperproject(BranchNameKey branch) {
+ return subscribedBranches.contains(branch);
+ }
+
+ /** See if a {@code branch} is a superproject branch affected. */
+ boolean hasSubscription(BranchNameKey branch) {
+ return targets.containsKey(branch);
+ }
+
+ /** Get all related {@code SubmoduleSubscription}s whose super branch is {@code branch}. */
+ ImmutableSet<SubmoduleSubscription> getSubscriptions(BranchNameKey branch) {
+ return targets.get(branch);
+ }
+
+ public interface Factory {
+ SubscriptionGraph compute(Set<BranchNameKey> updatedBranches) throws SubmoduleConflictException;
+ }
+
+ static class DefaultFactory implements Factory {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final ProjectCache projectCache;
+ private final GitModules.Factory gitmodulesFactory;
+ private final Map<BranchNameKey, GitModules> branchGitModules;
+ private final MergeOpRepoManager orm;
+
+ // Fields required to the constructor of SubscriptionGraph.
+ /** All affected branches, including those in superprojects and submodules. */
+ private final Set<BranchNameKey> affectedBranches;
+
+ /** @see SubscriptionGraph#targets */
+ private final SetMultimap<BranchNameKey, SubmoduleSubscription> targets;
+
+ /** @see SubscriptionGraph#branchesByProject */
+ private final SetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
+
+ /** @see SubscriptionGraph#subscribedBranches */
+ private final Set<BranchNameKey> subscribedBranches;
+
+ DefaultFactory(
+ GitModules.Factory gitmodulesFactory, ProjectCache projectCache, MergeOpRepoManager orm) {
+ this.gitmodulesFactory = gitmodulesFactory;
+ this.projectCache = projectCache;
+ this.orm = orm;
+ this.branchGitModules = new HashMap<>();
+
+ this.affectedBranches = new HashSet<>();
+ this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
+ this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build();
+ this.subscribedBranches = new HashSet<>();
+ }
+
+ @Override
+ public SubscriptionGraph compute(Set<BranchNameKey> updatedBranches)
+ throws SubmoduleConflictException {
+ return new SubscriptionGraph(
+ updatedBranches,
+ targets,
+ branchesByProject,
+ subscribedBranches,
+ calculateSubscriptionMaps(updatedBranches));
+ }
+
+ /**
+ * Calculate the internal maps used by the operation.
+ *
+ * <p>In addition to the return value, the following fields are populated as a side effect:
+ *
+ * <ul>
+ * <li>{@link #affectedBranches}
+ * <li>{@link #targets}
+ * <li>{@link #branchesByProject}
+ * <li>{@link #subscribedBranches}
+ * </ul>
+ *
+ * @return the ordered set to be stored in {@link #sortedBranches}.
+ */
+ private Set<BranchNameKey> calculateSubscriptionMaps(Set<BranchNameKey> updatedBranches)
+ throws SubmoduleConflictException {
+ logger.atFine().log("Calculating superprojects - submodules map");
+ LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>();
+ for (BranchNameKey updatedBranch : updatedBranches) {
+ if (allVisited.contains(updatedBranch)) {
+ continue;
+ }
+
+ searchForSuperprojects(updatedBranch, new LinkedHashSet<>(), allVisited);
+ }
+
+ // Since the searchForSuperprojects will add all branches (related or
+ // unrelated) and ensure the superproject's branches get added first before
+ // a submodule branch. Need remove all unrelated branches and reverse
+ // the order.
+ allVisited.retainAll(affectedBranches);
+ reverse(allVisited);
+ return allVisited;
+ }
+
+ private void searchForSuperprojects(
+ BranchNameKey current,
+ LinkedHashSet<BranchNameKey> currentVisited,
+ LinkedHashSet<BranchNameKey> allVisited)
+ throws SubmoduleConflictException {
+ logger.atFine().log("Now processing %s", current);
+
+ if (currentVisited.contains(current)) {
+ throw new SubmoduleConflictException(
+ "Branch level circular subscriptions detected: "
+ + CircularPathFinder.printCircularPath(currentVisited, current));
+ }
+
+ if (allVisited.contains(current)) {
+ return;
+ }
+
+ currentVisited.add(current);
+ try {
+ Collection<SubmoduleSubscription> subscriptions =
+ superProjectSubscriptionsForSubmoduleBranch(current);
+ for (SubmoduleSubscription sub : subscriptions) {
+ BranchNameKey superBranch = sub.getSuperProject();
+ searchForSuperprojects(superBranch, currentVisited, allVisited);
+ targets.put(superBranch, sub);
+ branchesByProject.put(superBranch.project(), superBranch);
+ affectedBranches.add(superBranch);
+ affectedBranches.add(sub.getSubmodule());
+ subscribedBranches.add(sub.getSubmodule());
+ }
+ } catch (IOException e) {
+ throw new StorageException("Cannot find superprojects for " + current, e);
+ }
+ currentVisited.remove(current);
+ allVisited.add(current);
+ }
+
+ private Collection<BranchNameKey> getDestinationBranches(BranchNameKey src, SubscribeSection s)
+ throws IOException {
+ Collection<BranchNameKey> ret = new HashSet<>();
+ logger.atFine().log("Inspecting SubscribeSection %s", s);
+ for (RefSpec r : s.getMatchingRefSpecs()) {
+ logger.atFine().log("Inspecting [matching] ref %s", r);
+ if (!r.matchSource(src.branch())) {
+ continue;
+ }
+ if (r.isWildcard()) {
+ // refs/heads/*[:refs/somewhere/*]
+ ret.add(
+ BranchNameKey.create(
+ s.getProject(), r.expandFromSource(src.branch()).getDestination()));
+ } else {
+ // e.g. refs/heads/master[:refs/heads/stable]
+ String dest = r.getDestination();
+ if (dest == null) {
+ dest = r.getSource();
+ }
+ ret.add(BranchNameKey.create(s.getProject(), dest));
+ }
+ }
+
+ for (RefSpec r : s.getMultiMatchRefSpecs()) {
+ logger.atFine().log("Inspecting [all] ref %s", r);
+ if (!r.matchSource(src.branch())) {
+ continue;
+ }
+ OpenRepo or;
+ try {
+ or = orm.getRepo(s.getProject());
+ } catch (NoSuchProjectException e) {
+ // A project listed a non existent project to be allowed
+ // to subscribe to it. Allow this for now, i.e. no exception is
+ // thrown.
+ continue;
+ }
+
+ for (Ref ref : or.repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS)) {
+ if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
+ continue;
+ }
+ BranchNameKey b = BranchNameKey.create(s.getProject(), ref.getName());
+ if (!ret.contains(b)) {
+ ret.add(b);
+ }
+ }
+ }
+ logger.atFine().log("Returning possible branches: %s for project %s", ret, s.getProject());
+ return ret;
+ }
+
+ private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
+ BranchNameKey srcBranch) throws IOException {
+ logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
+ Collection<SubmoduleSubscription> ret = new ArrayList<>();
+ Project.NameKey srcProject = srcBranch.project();
+ for (SubscribeSection s :
+ projectCache
+ .get(srcProject)
+ .orElseThrow(illegalState(srcProject))
+ .getSubscribeSections(srcBranch)) {
+ logger.atFine().log("Checking subscribe section %s", s);
+ Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s);
+ for (BranchNameKey targetBranch : branches) {
+ Project.NameKey targetProject = targetBranch.project();
+ try {
+ OpenRepo or = orm.getRepo(targetProject);
+ ObjectId id = or.repo.resolve(targetBranch.branch());
+ if (id == null) {
+ logger.atFine().log("The branch %s doesn't exist.", targetBranch);
+ continue;
+ }
+ } catch (NoSuchProjectException e) {
+ logger.atFine().log("The project %s doesn't exist", targetProject);
+ continue;
+ }
+
+ GitModules m = branchGitModules.get(targetBranch);
+ if (m == null) {
+ m = gitmodulesFactory.create(targetBranch, orm);
+ branchGitModules.put(targetBranch, m);
+ }
+ ret.addAll(m.subscribedTo(srcBranch));
+ }
+ }
+ logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret);
+ return ret;
+ }
+
+ private static <T> void reverse(LinkedHashSet<T> set) {
+ if (set == null) {
+ return;
+ }
+
+ Deque<T> q = new ArrayDeque<>(set);
+ set.clear();
+
+ while (!q.isEmpty()) {
+ set.add(q.removeLast());
+ }
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index cf349ab..7b99a55 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -31,7 +31,7 @@
@ConfigSuite.Config
public static Config elasticsearchV7() {
- return getConfig(ElasticVersion.V7_7);
+ return getConfig(ElasticVersion.V7_8);
}
@Override
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 2901361..75950e2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1344,11 +1344,11 @@
assertThat(messages).hasSize(3);
String last = Iterables.getLast(messages);
if (getSubmitType() == SubmitType.CHERRY_PICK) {
- assertThat(last).startsWith("Change has been successfully cherry-picked as ");
+ assertThat(last).startsWith("Change has been successfully cherry-picked as");
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertThat(last).startsWith("Change has been successfully rebased and submitted as");
} else {
- assertThat(last).isEqualTo("Change has been successfully merged by Gerrit User 1000000");
+ assertThat(last).isEqualTo("Change has been successfully merged");
}
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index b539bf8..43a5ceb 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -33,7 +33,7 @@
@ConfigSuite.Config
public static Config elasticsearchV7() {
- return getConfig(ElasticVersion.V7_7);
+ return getConfig(ElasticVersion.V7_8);
}
@Override
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index c20650f..f7a806b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -60,6 +60,8 @@
return "blacktop/elasticsearch:7.6.2";
case V7_7:
return "blacktop/elasticsearch:7.7.1";
+ case V7_8:
+ return "blacktop/elasticsearch:7.8.0";
}
throw new IllegalStateException("No tests for version: " + version.name());
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index a015103..4826490 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -36,7 +36,7 @@
public static void startIndexService() {
if (container == null) {
// Only start Elasticsearch once
- container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
+ container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index b1de591..d9a4d2e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -46,7 +46,7 @@
public static void startIndexService() {
if (container == null) {
// Only start Elasticsearch once
- container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
+ container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
client = HttpAsyncClients.createDefault();
client.start();
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 2e382d4..0fc96f8 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -36,7 +36,7 @@
public static void startIndexService() {
if (container == null) {
// Only start Elasticsearch once
- container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
+ container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 87a14da..1e56af9 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -36,7 +36,7 @@
public static void startIndexService() {
if (container == null) {
// Only start Elasticsearch once
- container = ElasticContainer.createAndStart(ElasticVersion.V7_7);
+ container = ElasticContainer.createAndStart(ElasticVersion.V7_8);
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index c9a7a46..ac7f33b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -54,6 +54,9 @@
assertThat(ElasticVersion.forVersion("7.7.0")).isEqualTo(ElasticVersion.V7_7);
assertThat(ElasticVersion.forVersion("7.7.1")).isEqualTo(ElasticVersion.V7_7);
+
+ assertThat(ElasticVersion.forVersion("7.8.0")).isEqualTo(ElasticVersion.V7_8);
+ assertThat(ElasticVersion.forVersion("7.8.1")).isEqualTo(ElasticVersion.V7_8);
}
@Test
@@ -81,6 +84,7 @@
assertThat(ElasticVersion.V7_5.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
assertThat(ElasticVersion.V7_6.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
assertThat(ElasticVersion.V7_7.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
+ assertThat(ElasticVersion.V7_8.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
}
@Test
@@ -96,6 +100,7 @@
assertThat(ElasticVersion.V7_5.isV6OrLater()).isTrue();
assertThat(ElasticVersion.V7_6.isV6OrLater()).isTrue();
assertThat(ElasticVersion.V7_7.isV6OrLater()).isTrue();
+ assertThat(ElasticVersion.V7_8.isV6OrLater()).isTrue();
}
@Test
@@ -111,5 +116,6 @@
assertThat(ElasticVersion.V7_5.isV7OrLater()).isTrue();
assertThat(ElasticVersion.V7_6.isV7OrLater()).isTrue();
assertThat(ElasticVersion.V7_7.isV7OrLater()).isTrue();
+ assertThat(ElasticVersion.V7_8.isV7OrLater()).isTrue();
}
}
diff --git a/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java b/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
new file mode 100644
index 0000000..2679829
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2020 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.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.AllowRenderInFrameFilter.X_FRAME_OPTIONS_HEADER_NAME;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.gerrit.httpd.AllowRenderInFrameFilter.XFrameOption;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AllowRenderInFrameFilterTest {
+
+ private Config cfg = new Config();
+ @Mock ServletRequest request;
+ @Mock HttpServletResponse response;
+ @Mock FilterChain filterChain;
+
+ @Test
+ public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalse()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
+ }
+
+ @Test
+ public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalseAndXFormOptionIsSAMEORIGIN()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
+ }
+
+ @Test
+ public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalseAndXFormOptionIsALLOW()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.ALLOW);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
+ }
+
+ @Test
+ public void shouldRestrictAccessToSAMEORIGINWhenCanRenderInFrameIsTrue()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
+ }
+
+ @Test
+ public void shouldSkipHeaderWhenCanRenderInFrameIsTrueAndXFormOptionIsALLOW()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.ALLOW);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, never()).addHeader(eq(X_FRAME_OPTIONS_HEADER_NAME), anyString());
+ }
+
+ @Test
+ public void shouldRestrictAccessToSAMEORIGINWhenCanRenderInFrameIsTrueAndXFormOptionIsSAMEORIGIN()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
+ }
+
+ @Test
+ public void shouldIgnoreXFrameOriginCaseSensitivity() throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setString("gerrit", null, "xframeOption", "sameOrigin");
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenUnknownXFormOptionValue() {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setString("gerrit", null, "xframeOption", "unsupported value");
+
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> new AllowRenderInFrameFilter(cfg));
+ assertThat(e).hasMessageThat().contains("gerrit.xframeOption=unsupported value");
+ }
+}
diff --git a/javatests/com/google/gerrit/server/submit/SubscriptionGraphTest.java b/javatests/com/google/gerrit/server/submit/SubscriptionGraphTest.java
new file mode 100644
index 0000000..dbcc209
--- /dev/null
+++ b/javatests/com/google/gerrit/server/submit/SubscriptionGraphTest.java
@@ -0,0 +1,214 @@
+// Copyright (C) 2020 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.submit;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmoduleSubscription;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.submit.SubscriptionGraph.DefaultFactory;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public class SubscriptionGraphTest {
+ private static final String TEST_PATH = "test/path";
+ private static final Project.NameKey SUPER_PROJECT = Project.nameKey("Superproject");
+ private static final Project.NameKey SUB_PROJECT = Project.nameKey("Subproject");
+ private static final BranchNameKey SUPER_BRANCH =
+ BranchNameKey.create(SUPER_PROJECT, "refs/heads/one");
+ private static final BranchNameKey SUB_BRANCH =
+ BranchNameKey.create(SUB_PROJECT, "refs/heads/one");
+ private InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager();
+ private MergeOpRepoManager mergeOpRepoManager;
+
+ @Mock GitModules.Factory mockGitModulesFactory = mock(GitModules.Factory.class);
+ @Mock ProjectCache mockProjectCache = mock(ProjectCache.class);
+ @Mock ProjectState mockProjectState = mock(ProjectState.class);
+
+ @Before
+ public void setUp() throws Exception {
+ when(mockProjectCache.get(any())).thenReturn(Optional.of(mockProjectState));
+ mergeOpRepoManager = new MergeOpRepoManager(repoManager, mockProjectCache, null, null);
+
+ GitModules emptyMockGitModules = mock(GitModules.class);
+ when(emptyMockGitModules.subscribedTo(any())).thenReturn(ImmutableSet.of());
+ when(mockGitModulesFactory.create(any(), any())).thenReturn(emptyMockGitModules);
+
+ TestRepository<Repository> superProject = createRepo(SUPER_PROJECT);
+ TestRepository<Repository> submoduleProject = createRepo(SUB_PROJECT);
+
+ // Make sure that SUPER_BRANCH and SUB_BRANCH can be subscribed.
+ allowSubscription(SUPER_BRANCH);
+ allowSubscription(SUB_BRANCH);
+
+ setSubscription(SUB_BRANCH, ImmutableList.of(SUPER_BRANCH));
+ setSubscription(SUPER_BRANCH, ImmutableList.of());
+ createBranch(
+ superProject, SUPER_BRANCH, superProject.commit().message("Initial commit").create());
+ createBranch(
+ submoduleProject, SUB_BRANCH, submoduleProject.commit().message("Initial commit").create());
+ }
+
+ @Test
+ public void oneSuperprojectOneSubmodule() throws Exception {
+ SubscriptionGraph.Factory factory =
+ new DefaultFactory(mockGitModulesFactory, mockProjectCache, mergeOpRepoManager);
+ SubscriptionGraph subscriptionGraph = factory.compute(ImmutableSet.of(SUB_BRANCH));
+
+ assertThat(subscriptionGraph.getAffectedSuperProjects()).containsExactly(SUPER_PROJECT);
+ assertThat(subscriptionGraph.getAffectedSuperBranches(SUPER_PROJECT))
+ .containsExactly(SUPER_BRANCH);
+ assertThat(subscriptionGraph.getSubscriptions(SUPER_BRANCH))
+ .containsExactly(new SubmoduleSubscription(SUPER_BRANCH, SUB_BRANCH, TEST_PATH));
+ assertThat(subscriptionGraph.hasSuperproject(SUB_BRANCH)).isTrue();
+ assertThat(subscriptionGraph.getSortedSuperprojectAndSubmoduleBranches())
+ .containsExactly(SUB_BRANCH, SUPER_BRANCH)
+ .inOrder();
+ }
+
+ @Test
+ public void circularSubscription() throws Exception {
+ SubscriptionGraph.Factory factory =
+ new DefaultFactory(mockGitModulesFactory, mockProjectCache, mergeOpRepoManager);
+ setSubscription(SUPER_BRANCH, ImmutableList.of(SUB_BRANCH));
+ SubmoduleConflictException e =
+ assertThrows(
+ SubmoduleConflictException.class, () -> factory.compute(ImmutableSet.of(SUB_BRANCH)));
+
+ String expectedErrorMessage =
+ "Subproject,refs/heads/one->Superproject,refs/heads/one->Subproject,refs/heads/one";
+ assertThat(e).hasMessageThat().contains(expectedErrorMessage);
+ }
+
+ @Test
+ public void multipleSuperprojectsToMultipleSubmodules() throws Exception {
+ // Create superprojects and subprojects.
+ Project.NameKey superProject1 = Project.nameKey("superproject1");
+ Project.NameKey superProject2 = Project.nameKey("superproject2");
+ Project.NameKey subProject1 = Project.nameKey("subproject1");
+ Project.NameKey subProject2 = Project.nameKey("subproject2");
+ TestRepository<Repository> superProjectRepo1 = createRepo(superProject1);
+ TestRepository<Repository> superProjectRepo2 = createRepo(superProject2);
+ TestRepository<Repository> submoduleRepo1 = createRepo(subProject1);
+ TestRepository<Repository> submoduleRepo2 = createRepo(subProject2);
+
+ // Initialize super branches.
+ BranchNameKey superBranch1 = BranchNameKey.create(superProject1, "refs/heads/one");
+ BranchNameKey superBranch2 = BranchNameKey.create(superProject2, "refs/heads/one");
+ createBranch(
+ superProjectRepo1,
+ superBranch1,
+ superProjectRepo1.commit().message("Initial commit").create());
+ createBranch(
+ superProjectRepo2,
+ superBranch2,
+ superProjectRepo2.commit().message("Initial commit").create());
+
+ // Initialize sub branches.
+ BranchNameKey submoduleBranch1 = BranchNameKey.create(subProject1, "refs/heads/one");
+ BranchNameKey submoduleBranch2 = BranchNameKey.create(subProject1, "refs/heads/two");
+ BranchNameKey submoduleBranch3 = BranchNameKey.create(subProject2, "refs/heads/one");
+ createBranch(
+ submoduleRepo1, submoduleBranch1, submoduleRepo1.commit().message("Commit1").create());
+ createBranch(
+ submoduleRepo1, submoduleBranch2, submoduleRepo1.commit().message("Commit2").create());
+ createBranch(
+ submoduleRepo2, submoduleBranch3, submoduleRepo2.commit().message("Commit1").create());
+
+ allowSubscription(submoduleBranch1);
+ allowSubscription(submoduleBranch2);
+ allowSubscription(submoduleBranch3);
+
+ // Initialize subscriptions.
+ setSubscription(submoduleBranch1, ImmutableList.of(superBranch1, superBranch2));
+ setSubscription(submoduleBranch2, ImmutableList.of(superBranch1));
+ setSubscription(submoduleBranch3, ImmutableList.of(superBranch1, superBranch2));
+
+ SubscriptionGraph.Factory factory =
+ new DefaultFactory(mockGitModulesFactory, mockProjectCache, mergeOpRepoManager);
+ SubscriptionGraph subscriptionGraph =
+ factory.compute(ImmutableSet.of(submoduleBranch1, submoduleBranch2));
+
+ assertThat(subscriptionGraph.getAffectedSuperProjects())
+ .containsExactly(superProject1, superProject2);
+ assertThat(subscriptionGraph.getAffectedSuperBranches(superProject1))
+ .containsExactly(superBranch1);
+ assertThat(subscriptionGraph.getAffectedSuperBranches(superProject2))
+ .containsExactly(superBranch2);
+
+ assertThat(subscriptionGraph.getSubscriptions(superBranch1))
+ .containsExactly(
+ new SubmoduleSubscription(superBranch1, submoduleBranch1, TEST_PATH),
+ new SubmoduleSubscription(superBranch1, submoduleBranch2, TEST_PATH));
+ assertThat(subscriptionGraph.getSubscriptions(superBranch2))
+ .containsExactly(new SubmoduleSubscription(superBranch2, submoduleBranch1, TEST_PATH));
+
+ assertThat(subscriptionGraph.hasSuperproject(submoduleBranch1)).isTrue();
+ assertThat(subscriptionGraph.hasSuperproject(submoduleBranch2)).isTrue();
+ assertThat(subscriptionGraph.hasSuperproject(submoduleBranch3)).isFalse();
+
+ assertThat(subscriptionGraph.getSortedSuperprojectAndSubmoduleBranches())
+ .containsExactly(submoduleBranch2, submoduleBranch1, superBranch2, superBranch1)
+ .inOrder();
+ }
+
+ private TestRepository<Repository> createRepo(Project.NameKey project) throws Exception {
+ Repository repo = repoManager.createRepository(project);
+ return new TestRepository<>(repo);
+ }
+
+ private void createBranch(TestRepository<Repository> repo, BranchNameKey branch, RevCommit commit)
+ throws Exception {
+ repo.update(branch.branch(), commit);
+ }
+
+ private void allowSubscription(BranchNameKey branch) {
+ SubscribeSection s = new SubscribeSection(branch.project());
+ s.addMultiMatchRefSpec("refs/heads/*:refs/heads/*");
+ when(mockProjectState.getSubscribeSections(branch)).thenReturn(ImmutableSet.of(s));
+ }
+
+ private void setSubscription(
+ BranchNameKey submoduleBranch, List<BranchNameKey> superprojectBranches) {
+ List<SubmoduleSubscription> subscriptions =
+ superprojectBranches.stream()
+ .map(
+ (targetBranch) ->
+ new SubmoduleSubscription(targetBranch, submoduleBranch, TEST_PATH))
+ .collect(Collectors.toList());
+ GitModules mockGitModules = mock(GitModules.class);
+ when(mockGitModules.subscribedTo(submoduleBranch)).thenReturn(subscriptions);
+ when(mockGitModulesFactory.create(submoduleBranch, mergeOpRepoManager))
+ .thenReturn(mockGitModules);
+ }
+}
diff --git a/modules/jgit b/modules/jgit
index 8a44216..8e79d5a 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 8a44216e8bdad1a13ce637b1395467600870849a
+Subproject commit 8e79d5a290843b929f073a536a0d678fc74382ca
diff --git a/plugins/replication b/plugins/replication
index 5838489..dc6762f 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 583848945b150503fb76fb780f53bc5c2b42546c
+Subproject commit dc6762f60e561868cabe4824c4c13291eeaa068d
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
index b8d54a4..46dfaf2 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import {descendedFromClass} from '../../utils/dom-util.js';
+
export const DomUtilBehavior = {
/**
* Are any ancestors of the element (or the element itself) members of the
@@ -28,13 +30,9 @@
* @return {boolean}
*/
descendedFromClass(element, className, opt_stopElement) {
- let isDescendant = element.classList.contains(className);
- while (!isDescendant && element.parentElement &&
- (!opt_stopElement || element.parentElement !== opt_stopElement)) {
- isDescendant = element.classList.contains(className);
- element = element.parentElement;
- }
- return isDescendant;
+ console.warn('DomUtilBehavior is deprecated.' +
+ 'Use descendedFromClass from utils directly.');
+ return descendedFromClass(element, className, opt_stopElement);
},
};
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index ca31ee8..49f27bc 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -507,7 +507,8 @@
modifierPressed(e) {
e = getKeyboardEvent(e);
- return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
+ return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey ||
+ !!this._inGoKeyMode();
},
isModifierPressed(e, modifier) {
@@ -580,11 +581,14 @@
this._addOwnKeyBindings(key, shortcuts[key]);
}
+ // each component that uses this behaviour must be aware if go key is
+ // pressed or not, since it needs to check it as a modifier
+ this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown');
+ this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp');
+
// If any of the shortcuts utilized GO_KEY, then they are handled
// directly by this behavior.
if (this._shortcut_go_table.size > 0) {
- this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown');
- this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp');
this._shortcut_go_table.forEach((handler, key) => {
this.addOwnKeyBinding(key, '_handleGoAction');
});
@@ -611,12 +615,15 @@
},
_handleGoKeyDown(e) {
- if (this.modifierPressed(e)) { return; }
this._shortcut_go_key_last_pressed = Date.now();
},
_handleGoKeyUp(e) {
- this._shortcut_go_key_last_pressed = null;
+ // Set go_key_last_pressed to null `GO_KEY_TIMEOUT_MS` after keyup event
+ // so that users can trigger `g + i` by pressing g and i quickly.
+ setTimeout(() => {
+ this._shortcut_go_key_last_pressed = null;
+ }, GO_KEY_TIMEOUT_MS);
},
_inGoKeyMode() {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
deleted file mode 100644
index d161423..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-button/gr-button.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-repo-command_html.js';
-
-/** @extends PolymerElement */
-class GrRepoCommand extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-repo-command'; }
-
- static get properties() {
- return {
- title: String,
- disabled: Boolean,
- tooltip: String,
- };
- }
-
- /**
- * Fired when command button is tapped.
- *
- * @event command-tap
- */
-
- _onCommandTap() {
- this.dispatchEvent(
- new CustomEvent('command-tap', {bubbles: true, composed: true}));
- }
-}
-
-customElements.define(GrRepoCommand.is, GrRepoCommand);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
deleted file mode 100644
index a73f071..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-command</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
- <template>
- <gr-repo-command></gr-repo-command>
- </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-repo-command.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-suite('gr-repo-command tests', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('dispatched command-tap on button tap', done => {
- element.addEventListener('command-tap', () => {
- done();
- });
- MockInteractions.tap(
- dom(element.root).querySelector('gr-button'));
- });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 25f656d..4ab1b98 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -24,7 +24,6 @@
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-change-dialog/gr-create-change-dialog.js';
-import '../gr-repo-command/gr-repo-command.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
index 5ff7dde..4e826aa 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
@@ -45,13 +45,6 @@
white-space: nowrap;
width: 100%;
}
- .content a {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
.comments,
.reviewers {
white-space: nowrap;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index bd78ae9..baf9920 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -168,7 +168,6 @@
/** @override */
ready() {
super.ready();
- this._ensureAttribute('tabindex', 0);
this.$.restAPI.getConfig().then(config => {
this._config = config;
});
@@ -296,6 +295,11 @@
return idx == selectedIndex;
}
+ _computeTabIndex(sectionIndex, index, selectedIndex) {
+ return this._computeItemSelected(sectionIndex, index, selectedIndex)
+ ? 0 : undefined;
+ }
+
_computeItemNeedsReview(account, change, showReviewedState) {
return showReviewedState && !change.reviewed &&
!change.work_in_progress &&
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
index f7c50e6..2e50cd8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
@@ -138,7 +138,7 @@
visible-change-table-columns="[[visibleChangeTableColumns]]"
show-number="[[showNumber]]"
show-star="[[showStar]]"
- tabindex="0"
+ tabindex$="[[_computeTabIndex(sectionIndex, index, selectedIndex)]]"
label-names="[[labelNames]]"
></gr-change-list-item>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 9d3b455..b098a93 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -151,7 +151,7 @@
_currentParents: {
type: Array,
- computed: '_computeParents(revision)',
+ computed: '_computeParents(change, revision)',
},
/** @type {?} */
@@ -493,9 +493,11 @@
return null;
}
- _computeParents(revision) {
+ _computeParents(change, revision) {
if (!revision || !revision.commit) {
- return undefined;
+ if (!change || !change.current_revision) { return []; }
+ revision = change.revisions[change.current_revision];
+ if (!revision || !revision.commit) { return []; }
}
return revision.commit.parents;
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 81b9bde..8e780d7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -426,20 +426,38 @@
});
test('_computeParents', () => {
- const revision = {commit: {parents: [{commit: '123', subject: 'abc'}]}};
- assert.isUndefined(element._computeParents({}));
- assert.equal(element._computeParents(revision), revision.commit.parents);
+ const parents = [{commit: '123', subject: 'abc'}];
+ const revision = {commit: {parents}};
+ assert.deepEqual(element._computeParents({}, {}), []);
+ assert.equal(element._computeParents(null, revision), parents);
+ const change = current_revision => {
+ return {current_revision, revisions: {456: revision}};
+ };
+ assert.deepEqual(element._computeParents(change(null), null), []);
+ const change_bad_revision = change('789');
+ assert.deepEqual(element._computeParents(change_bad_revision, {}), []);
+ const change_no_commit = {current_revision: '456', revisions: {456: {}}};
+ assert.deepEqual(element._computeParents(change_no_commit, null), []);
+ const change_good = change('456');
+ assert.equal(element._computeParents(change_good, null), parents);
});
test('_currentParents', () => {
- element.revision = {
- commit: {parents: [{commit: '123', subject: 'abc'}]},
+ const revision = parent => {
+ return {commit: {parents: [{commit: parent, subject: 'abc'}]}};
};
- assert.equal(element._currentParents[0].commit, '123');
- element.revision = {
- commit: {parents: [{commit: '12345', subject: 'abc'}]},
+ element.change = {
+ current_revision: '456',
+ revisions: {456: revision('111')},
};
- assert.equal(element._currentParents[0].commit, '12345');
+ element.revision = revision('222');
+ assert.equal(element._currentParents[0].commit, '222');
+ element.revision = revision('333');
+ assert.equal(element._currentParents[0].commit, '333');
+ element.revision = null;
+ assert.equal(element._currentParents[0].commit, '111');
+ element.change = {current_revision: null};
+ assert.deepEqual(element._currentParents, []);
});
test('_computeParentsLabel', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index 78627a7..d2d6037 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -104,7 +104,7 @@
}
_computeRequirementIcon(requirementStatus) {
- return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
+ return requirementStatus ? 'gr-icons:check' : 'gr-icons:schedule';
}
_computeLabels(labelsRecord) {
@@ -132,7 +132,7 @@
_computeLabelIcon(labelInfo) {
if (labelInfo.approved) { return 'gr-icons:check'; }
if (labelInfo.rejected) { return 'gr-icons:close'; }
- return 'gr-icons:hourglass';
+ return 'gr-icons:schedule';
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index e100f91..276e821 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -51,13 +51,13 @@
assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
assert.equal(element._computeRequirementIcon(false),
- 'gr-icons:hourglass');
+ 'gr-icons:schedule');
});
test('label computed fields', () => {
assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
- assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass');
+ assert.equal(element._computeLabelIcon({}), 'gr-icons:schedule');
assert.equal(element._computeLabelClass({approved: []}), 'approved');
assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
@@ -90,7 +90,7 @@
assert.equal(element._requiredLabels.length, 1);
assert.equal(element._optionalLabels[0].label, 'opt_test');
- assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
+ assert.equal(element._optionalLabels[0].icon, 'gr-icons:schedule');
assert.equal(element._optionalLabels[0].style, '');
assert.ok(element._optionalLabels[0].labelInfo);
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index dc207dc..799adde 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -57,7 +57,7 @@
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
@@ -2003,7 +2003,7 @@
_computeShowRelatedToggle() {
// Make sure the max height has been applied, since there is now content
// to populate.
- if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
+ if (!getComputedStyleValue('--relation-chain-max-height', this)) {
this._updateRelatedChangeMaxHeight();
}
// Prevents showMore from showing when click on related change, since the
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 1362fe3..faa81b6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -24,7 +24,7 @@
import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
@@ -320,7 +320,7 @@
});
const getCustomCssValue =
- cssParam => util.getComputedStyleValue(cssParam, element);
+ cssParam => getComputedStyleValue(cssParam, element);
test('_handleMessageAnchorTap', () => {
element._changeNum = '1';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 894f0cc..fbae39f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -1279,6 +1279,14 @@
_renderInOrder(files, diffElements, initialCount) {
let iter = 0;
+ for (const file of files) {
+ const path = file.path;
+ const diffElem = this._findDiffByPath(path, diffElements);
+ if (diffElem) {
+ diffElem.prefetchDiff();
+ }
+ }
+
return (new Promise(resolve => {
this.dispatchEvent(new CustomEvent('reload-drafts', {
detail: {resolve},
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index df7cb00..a2714d9 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -329,6 +329,12 @@
as="headerEndpoint"
>
<gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
+ <gr-endpoint-param
+ name="change"
+ value="[[change]]"
+ ></gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" value="[[patchRange]]">
+ </gr-endpoint-param>
</gr-endpoint-decorator>
</template>
</template>
@@ -381,6 +387,8 @@
as="contentEndpoint"
>
<gr-endpoint-decorator name="[[contentEndpoint]]" role="gridcell">
+ <gr-endpoint-param name="change" value="[[change]]">
+ </gr-endpoint-param>
<gr-endpoint-param name="changeNum" value="[[changeNum]]">
</gr-endpoint-param>
<gr-endpoint-param name="patchRange" value="[[patchRange]]">
@@ -439,7 +447,7 @@
<div class="comments desktop">
<span class="drafts"
><!-- This comments ensure that span is empty when the function
- returns empty string.
+ returns empty string.
-->[[_computeDraftsString(changeComments, patchRange,
file.__path)]]<!-- This comments ensure that span is empty when
the function returns empty string.
@@ -458,7 +466,7 @@
Without this span, screen readers don't navigate correctly inside
table, because empty div doesn't rendered. For example, VoiceOver
jumps back to the whole table.
- We can use   instead, but it sounds worse.
+ We can use   instead, but it sounds worse.
-->
No comments
</span>
@@ -553,6 +561,8 @@
>
<div class$="[[_computeClass('', file.__path)]]" role="gridcell">
<gr-endpoint-decorator name="[[contentEndpoint]]">
+ <gr-endpoint-param name="change" value="[[change]]">
+ </gr-endpoint-param>
<gr-endpoint-param name="changeNum" value="[[changeNum]]">
</gr-endpoint-param>
<gr-endpoint-param name="patchRange" value="[[patchRange]]">
@@ -575,7 +585,7 @@
>
<!-- Do not use input type="checkbox" with hidden input and
visible label here. Screen readers don't read/interract
- correctly with such input.
+ correctly with such input.
-->
<span
class="reviewedSwitch"
@@ -611,7 +621,7 @@
<div class="show-hide" role="gridcell">
<!-- Do not use input type="checkbox" with hidden input and
visible label here. Screen readers don't read/interract
- correctly with such input.
+ correctly with such input.
-->
<span
class="show-hide"
@@ -683,6 +693,12 @@
as="summaryEndpoint"
>
<gr-endpoint-decorator name="[[summaryEndpoint]]">
+ <gr-endpoint-param
+ name="change"
+ value="[[change]]"
+ ></gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" value="[[patchRange]]">
+ </gr-endpoint-param>
</gr-endpoint-decorator>
</template>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index f93cfe1..ad0d1cf 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -95,6 +95,7 @@
});
stub('gr-diff-host', {
reload() { return Promise.resolve(); },
+ prefetchDiff() {},
});
// Element must be wrapped in an element with direct access to the
@@ -1106,6 +1107,7 @@
reload() {
done();
},
+ prefetchDiff() {},
cancel() {},
getCursorStops() { return []; },
addEventListener(eventName, callback) {
@@ -1163,6 +1165,7 @@
const diffs = [{
path: 'p0',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(callCount++, 2);
return Promise.resolve();
@@ -1170,6 +1173,7 @@
}, {
path: 'p1',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(callCount++, 1);
return Promise.resolve();
@@ -1177,6 +1181,7 @@
}, {
path: 'p2',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(callCount++, 0);
return Promise.resolve();
@@ -1199,6 +1204,7 @@
const diffs = [{
path: 'p0',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(reviewStub.callCount, 2);
assert.equal(callCount++, 2);
@@ -1207,6 +1213,7 @@
}, {
path: 'p1',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(reviewStub.callCount, 1);
assert.equal(callCount++, 1);
@@ -1215,6 +1222,7 @@
}, {
path: 'p2',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(reviewStub.callCount, 0);
assert.equal(callCount++, 0);
@@ -1237,6 +1245,7 @@
const diffs = [{
path: 'p',
style: {},
+ prefetchDiff() {},
reload() { return Promise.resolve(); },
}];
@@ -1566,6 +1575,7 @@
});
stub('gr-diff-host', {
reload() { return Promise.resolve(); },
+ prefetchDiff() {},
});
// Element must be wrapped in an element with direct access to the
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 903552a..23a4c55 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -90,7 +90,9 @@
return undefined;
}
- const links = [{name: 'Settings', url: '/settings/'}];
+ const links = [];
+ links.push({name: 'Settings', url: '/settings/'});
+ links.push({name: 'Keyboard Shortcuts', id: 'shortcuts'});
if (switchAccountUrl) {
const replacements = {path};
const url = this._interpolateUrl(switchAccountUrl, replacements);
@@ -107,6 +109,11 @@
];
}
+ _handleShortcutsTap(e) {
+ this.dispatchEvent(new CustomEvent('show-keyboard-shortcuts',
+ {bubbles: true, composed: true}));
+ }
+
_handleLocationChange() {
this._path =
window.location.pathname +
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
index b47894e..5db7923 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
@@ -37,6 +37,7 @@
link=""
items="[[links]]"
top-content="[[topContent]]"
+ on-tap-item-shortcuts="_handleShortcutsTap"
horizontal-align="right"
>
<span hidden$="[[_hasAvatars]]" hidden="">[[_accountName(account)]]</span>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index 6c8ed68..a366d09 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -84,12 +84,12 @@
assert.isUndefined(element._getLinks(null));
// No switch account link.
- assert.equal(element._getLinks(null, '').length, 2);
+ assert.equal(element._getLinks(null, '').length, 3);
// Unparameterized switch account link.
let links = element._getLinks('/switch-account', '');
- assert.equal(links.length, 3);
- assert.deepEqual(links[1], {
+ assert.equal(links.length, 4);
+ assert.deepEqual(links[2], {
name: 'Switch account',
url: '/switch-account',
external: true,
@@ -97,8 +97,8 @@
// Parameterized switch account link.
links = element._getLinks('/switch-account${path}', '/c/123');
- assert.equal(links.length, 3);
- assert.deepEqual(links[1], {
+ assert.equal(links.length, 4);
+ assert.deepEqual(links[2], {
name: 'Switch account',
url: '/switch-account/c/123',
external: true,
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 9f6d050..6cd2491 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -28,7 +28,6 @@
import {htmlTemplate} from './gr-error-manager_html.js';
import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
import {authService} from '../../shared/gr-rest-api-interface/gr-auth.js';
-import {gerritEventEmitter} from '../../shared/gr-event-emitter/gr-event-emitter.js';
import {appContext} from '../../../services/app-context.js';
const HIDE_ALERT_TIMEOUT_MS = 5000;
@@ -93,6 +92,7 @@
this._authErrorHandlerDeregistrationHook;
this.reporting = appContext.reportingService;
+ this.eventEmitter = appContext.eventEmitter;
}
/** @override */
@@ -106,7 +106,7 @@
this.listen(document, 'show-auth-required', '_handleAuthRequired');
this._authErrorHandlerDeregistrationHook =
- gerritEventEmitter.on('auth-error',
+ this.eventEmitter.on('auth-error',
event => {
this._handleAuthError(event.message, event.action);
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 655a4eb..1cabd82 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -315,15 +315,20 @@
}
if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
- const button = this._createElement('button');
- button.tabIndex = -1;
- td.appendChild(button);
-
// Both td and button need a number of classes/attributes for various
// selectors to work.
this._decorateLineEl(td, number, side);
td.classList.add('lineNum');
+
+ if (this._prefs.show_file_comment_button === false && number === 'FILE') {
+ return td;
+ }
+
+ const button = this._createElement('button');
+ td.appendChild(button);
+ button.tabIndex = -1;
this._decorateLineEl(button, number, side);
+
button.classList.add('lineNumButton');
button.textContent = number === 'FILE' ? 'File' : number;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 229ae43..665e4a6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -119,6 +119,30 @@
return null;
}
+ _toggleRangeElHighlight(threadEl, highlightRange = false) {
+ // We don't want to re-create the line just for highlighting the range which
+ // is creating annoying bugs: @see Issue 12934
+ // As gr-ranged-comment-layer now does not notify the layer re-render and
+ // lack of access to the thread or the lineEl from the ranged-comment-layer,
+ // need to update range class for styles here.
+ const currentLine = threadEl.assignedSlot.parentElement.previousSibling;
+ if (currentLine && currentLine.querySelector) {
+ if (highlightRange) {
+ const rangeNode = currentLine.querySelector('.range');
+ if (rangeNode) {
+ rangeNode.classList.add('rangeHighlight');
+ rangeNode.classList.remove('range');
+ }
+ } else {
+ const rangeNode = currentLine.querySelector('.rangeHighlight');
+ if (rangeNode) {
+ rangeNode.classList.remove('rangeHighlight');
+ rangeNode.classList.add('range');
+ }
+ }
+ }
+ }
+
_handleCommentThreadMouseenter(e) {
const threadEl = this._getThreadEl(e);
const index = this._indexForThreadEl(threadEl);
@@ -126,6 +150,8 @@
if (index !== undefined) {
this.set(['commentRanges', index, 'hovering'], true);
}
+
+ this._toggleRangeElHighlight(threadEl, /* highlightRange= */ true);
}
_handleCommentThreadMouseleave(e) {
@@ -135,6 +161,8 @@
if (index !== undefined) {
this.set(['commentRanges', index, 'hovering'], false);
}
+
+ this._toggleRangeElHighlight(threadEl, /* highlightRange= */ false);
}
_indexForThreadEl(threadEl) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index ef11247..cb299de 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -218,6 +218,11 @@
notify: true,
},
+ _fetchDiffPromise: {
+ type: Object,
+ value: null,
+ },
+
/** @type {?Object} */
_blame: {
type: Object,
@@ -535,8 +540,21 @@
!this.noAutoRender;
}
+ // TODO(milutin): Use rest-api with fetchCacheURL instead of this.
+ prefetchDiff() {
+ if (!!this.changeNum && !!this.patchRange && !!this.path
+ && this._fetchDiffPromise === null) {
+ this._fetchDiffPromise = this._getDiff();
+ }
+ }
+
/** @return {!Promise<!Object>} */
_getDiff() {
+ if (this._fetchDiffPromise !== null) {
+ const fetchDiffPromise = this._fetchDiffPromise;
+ this._fetchDiffPromise = null;
+ return fetchDiffPromise;
+ }
// Wrap the diff request in a new promise so that the error handler
// rejects the promise, allowing the error to be handled in the .catch.
return new Promise((resolve, reject) => {
@@ -793,6 +811,7 @@
}
threadEl.changeNum = this.changeNum;
threadEl.patchNum = thread.patchNum;
+ threadEl.showPatchset = false;
threadEl.lineNum = thread.lineNum;
const rootIdChangedListener = changeEvent => {
thread.rootId = changeEvent.detail.value;
@@ -906,6 +925,7 @@
return;
}
+ this._fetchDiffPromise = null;
if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
!noRenderOnPrefsChange) {
this.reload();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index b8f26b2..342c7ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -445,6 +445,19 @@
});
});
+ test('prefetch getDiff', done => {
+ const diffRestApiStub = sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve({content: []}));
+ element.changeNum = 123;
+ element.patchRange = {basePatchNum: 1, patchNum: 2};
+ element.path = 'file.txt';
+ element.prefetchDiff();
+ element._getDiff().then(() =>{
+ assert.isTrue(diffRestApiStub.calledOnce);
+ done();
+ });
+ });
+
test('_getDiff handles null diff responses', done => {
stub('gr-rest-api-interface', {
getDiff() { return Promise.resolve(null); },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 608e898..cfbe481 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -24,7 +24,7 @@
import {htmlTemplate} from './gr-diff-selection_html.js';
import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.js';
-import {util} from '../../../scripts/util.js';
+import {querySelectorAll} from '../../../utils/dom-util.js';
/**
* Possible CSS classes indicating the state of selection. Dynamically added/
@@ -203,7 +203,7 @@
}
_getSelection() {
- const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
+ const diffHosts = querySelectorAll(document.body, 'gr-diff');
if (!diffHosts.length) return window.getSelection();
const curDiffHost = diffHosts.find(diffHost => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 0510d3e..0942646 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -433,7 +433,8 @@
}
return Array.from(
- dom(this.root).querySelectorAll(':not(.contextControl) > .diff-row'));
+ dom(this.root).querySelectorAll(':not(.contextControl) > .diff-row'))
+ .filter(tr => tr.querySelector('button'));
}
/** @return {boolean} */
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index d497b9a..a10db97 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -21,7 +21,7 @@
import './gr-diff.js';
import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
import '@polymer/paper-button/paper-button.js';
@@ -82,14 +82,14 @@
element = basicFixture.instantiate();
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
flushAsynchronousOperations();
- assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+ assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
});
test('line limit without line_wrapping', () => {
element = basicFixture.instantiate();
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
flushAsynchronousOperations();
- assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
+ assert.isNotOk(getComputedStyleValue('--line-limit', element));
});
suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 3bf3697..c00c0fb 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -25,6 +25,7 @@
import {htmlTemplate} from './gr-patch-range-select_html.js';
import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import {appContext} from '../../../services/app-context.js';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -78,6 +79,11 @@
];
}
+ constructor() {
+ super();
+ this.reporting = appContext.reportingService;
+ }
+
_getShaForPatch(patch) {
return patch.sha.substring(0, 10);
}
@@ -285,8 +291,14 @@
const target = dom(e).localTarget;
if (target === this.$.patchNumDropdown) {
+ if (detail.patchNum === e.detail.value) return;
+ this.reporting.reportInteraction('right-patchset-changed',
+ {previous: detail.patchNum, current: e.detail.value});
detail.patchNum = e.detail.value;
} else {
+ if (detail.basePatchNum === e.detail.value) return;
+ this.reporting.reportInteraction('left-patchset-changed',
+ {previous: detail.basePatchNum, current: e.detail.value});
detail.basePatchNum = e.detail.value;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index 70aa1e7..9c9997c 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -133,10 +133,11 @@
if (record.path === 'commentRanges') {
this._rangesMap = {left: {}, right: {}};
for (const {side, range, hovering} of record.value) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- forLine.push({start, end, hovering});
- });
+ this._updateRangesMap({
+ side, range, hovering,
+ operation: (forLine, start, end, hovering) => {
+ forLine.push({start, end, hovering});
+ }});
}
}
@@ -147,12 +148,13 @@
// not the index, especially in polymer 1.
const {side, range, hovering} = this.get(match[1]);
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- const index = forLine.findIndex(lineRange =>
- lineRange.start === start && lineRange.end === end);
- forLine[index].hovering = hovering;
- });
+ this._updateRangesMap({
+ side, range, hovering, skipLayerUpdate: true,
+ operation: (forLine, start, end, hovering) => {
+ const index = forLine.findIndex(lineRange =>
+ lineRange.start === start && lineRange.end === end);
+ forLine[index].hovering = hovering;
+ }});
}
// If comments were spliced in or out.
@@ -160,26 +162,40 @@
for (const indexSplice of record.value.indexSplices) {
const removed = indexSplice.removed;
for (const {side, range, hovering} of removed) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end) => {
- const index = forLine.findIndex(lineRange =>
- lineRange.start === start && lineRange.end === end);
- forLine.splice(index, 1);
- });
+ this._updateRangesMap({
+ side, range, hovering, operation: (forLine, start, end) => {
+ const index = forLine.findIndex(lineRange =>
+ lineRange.start === start && lineRange.end === end);
+ forLine.splice(index, 1);
+ }});
}
const added = indexSplice.object.slice(
indexSplice.index, indexSplice.index + indexSplice.addedCount);
for (const {side, range, hovering} of added) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- forLine.push({start, end, hovering});
- });
+ this._updateRangesMap({
+ side, range, hovering,
+ operation: (forLine, start, end, hovering) => {
+ forLine.push({start, end, hovering});
+ }});
}
}
}
}
- _updateRangesMap(side, range, hovering, operation) {
+ /**
+ * @param {!Object} options
+ * @property {!string} options.side
+ * @property {boolean} options.hovering
+ * @property {boolean} options.skipLayerUpdate
+ * @property {!Function} options.operation
+ * @property {!{
+ * start_character: number,
+ * start_line: number,
+ * end_line: number,
+ * end_character: number}} options.range
+ */
+ _updateRangesMap(options) {
+ const {side, range, hovering, operation, skipLayerUpdate} = options;
const forSide = this._rangesMap[side] || (this._rangesMap[side] = {});
for (let line = range.start_line; line <= range.end_line; line++) {
const forLine = forSide[line] || (forSide[line] = []);
@@ -187,7 +203,9 @@
const end = line === range.end_line ? range.end_character : -1;
operation(forLine, start, end, hovering);
}
- this._notifyUpdateRange(range.start_line, range.end_line, side);
+ if (!skipLayerUpdate) {
+ this._notifyUpdateRange(range.start_line, range.end_line, side);
+ }
}
_getRangesForLine(line, side) {
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index ff1f4a7..37d1707 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -214,11 +214,8 @@
element.set(['commentRanges', 1, 'hovering'], true);
- assert.isTrue(notifyStub.called);
- const lastCall = notifyStub.lastCall;
- assert.equal(lastCall.args[0], 10);
- assert.equal(lastCall.args[1], 12);
- assert.equal(lastCall.args[2], 'right');
+ // notify will be skipped for hovering
+ assert.isFalse(notifyStub.called);
assert.isTrue(updateRangesMapSpy.called);
});
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index ecb40a5..4915eda 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -499,6 +499,10 @@
}
}
+ handleShowKeyboardShortcuts() {
+ this.$.keyboardShortcuts.open();
+ }
+
_showKeyboardShortcuts(e) {
// same shortcut should close the dialog if pressed again
// when dialog is open
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js
index 47139ce..75295bc 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.js
+++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -104,6 +104,7 @@
id="mainHeader"
search-query="{{params.query}}"
on-mobile-search="_mobileSearchToggle"
+ on-show-keyboard-shortcuts="handleShowKeyboardShortcuts"
login-url="[[_loginUrl]]"
>
</gr-main-header>
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.js
index 2c166f5..8c1161c 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -50,7 +50,7 @@
import {util} from '../scripts/util.js';
import page from 'page/page.mjs';
import {Auth} from './shared/gr-rest-api-interface/gr-auth.js';
-import {EventEmitter} from './shared/gr-event-interface/gr-event-interface.js';
+import {appContext} from '../services/app-context.js';
import {GrAdminApi} from './plugins/gr-admin-api/gr-admin-api.js';
import {GrAnnotationActionsContext} from './shared/gr-js-api-interface/gr-annotation-actions-context.js';
import {GrAnnotationActionsInterface} from './shared/gr-js-api-interface/gr-annotation-actions-js-api.js';
@@ -104,7 +104,7 @@
window.util = util;
window.page = page;
window.Auth = Auth;
- window.EventEmitter = EventEmitter;
+ window.EventEmitter = appContext.eventEmitter;
window.GrAdminApi = GrAdminApi;
window.GrAnnotationActionsContext = GrAnnotationActionsContext;
window.GrAnnotationActionsInterface = GrAnnotationActionsInterface;
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 410539c..1300955 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -38,6 +38,7 @@
import {htmlTemplate} from './gr-app_html.js';
import {SafeTypes} from '../behaviors/safe-types-behavior/safe-types-behavior.js';
import {initGerritPluginApi} from './shared/gr-js-api-interface/gr-gerrit.js';
+import {appContext} from '../services/app-context.js';
security.polymer_resin.install({
allowedIdentifierPrefixes: [''],
@@ -57,4 +58,4 @@
customElements.define(GrApp.is, GrApp);
initGlobalVariables();
-initGerritPluginApi();
+initGerritPluginApi(appContext);
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
index 752570f..1a2cd28 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -14,20 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../admin/gr-repo-command/gr-repo-command.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-Polymer({
- _template: html`
- <gr-repo-command title="[[title]]">
- </gr-repo-command>
-`,
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-plugin-repo-command_html.js';
- is: 'gr-plugin-repo-command',
+class GrPluginRepoCommand extends PolymerElement {
+ static get is() {
+ return 'gr-plugin-repo-command';
+ }
- properties: {
- title: String,
- repoName: String,
- config: Object,
- },
-});
+ static get properties() {
+ return {
+ title: String,
+ repoName: String,
+ config: Object,
+ };
+ }
+
+ static get template() {
+ return htmlTemplate;
+ }
+
+ _handleClick() {
+ this.dispatchEvent(
+ new CustomEvent('command-tap', {composed: true, bubbles: true})
+ );
+ }
+}
+
+customElements.define(GrPluginRepoCommand.is, GrPluginRepoCommand);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
similarity index 83%
rename from polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
rename to polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
index 50aca6d..1e5088b3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
@@ -23,12 +23,6 @@
margin-bottom: var(--spacing-xxl);
}
</style>
- <h3 class="heading-3">[[title]]</h3>
- <gr-button
- title$="[[tooltip]]"
- disabled$="[[disabled]]"
- on-click="_onCommandTap"
- >
- [[title]]
- </gr-button>
+ <h3>[[title]]</h3>
+ <gr-button on-click="_handleClick">[[title]]</gr-button>
`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index 32ae959..1af91fd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -73,13 +73,12 @@
const pluginCommand = element.shadowRoot
.querySelector('gr-plugin-repo-command');
assert.isOk(pluginCommand);
- const command = pluginCommand.shadowRoot
- .querySelector('gr-repo-command');
- assert.isOk(command);
- assert.equal(command.title, 'foo');
+ const btn = pluginCommand.shadowRoot
+ .querySelector('gr-button');
+ assert.isOk(btn);
+ assert.equal(btn.textContent, 'foo');
assert.isFalse(tapStub.called);
- MockInteractions.tap(command.shadowRoot
- .querySelector('gr-button'));
+ MockInteractions.tap(btn);
assert.isTrue(tapStub.called);
done();
});
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index bdac50f..346d84e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -23,7 +23,7 @@
import {htmlTemplate} from './gr-button_html.js';
import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {getEventPath} from '../../../utils/dom-util.js';
import {appContext} from '../../../services/app-context.js';
/**
@@ -113,7 +113,7 @@
}
this.reporting.reportInteraction('button-click',
- {path: util.getEventPath(e)});
+ {path: getEventPath(e)});
}
_disabledChanged(disabled) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index 7d5d170..fa686da 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -159,6 +159,10 @@
type: Boolean,
value: true,
},
+ showPatchset: {
+ type: Boolean,
+ value: true,
+ },
};
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
index 2bb5b66ea..837a58f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -116,6 +116,7 @@
patch-num="[[patchNum]]"
draft="[[_isDraft(comment)]]"
show-actions="[[_showActions]]"
+ show-patchset="[[showPatchset]]"
comment-side="[[comment.__commentSide]]"
side="[[comment.side]]"
project-config="[[_projectConfig]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 6697880..02fec5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -213,6 +213,10 @@
type: Boolean,
value: false,
},
+ showPatchset: {
+ type: Boolean,
+ value: true,
+ },
_respectfulReviewTip: String,
_respectfulTipDismissed: {
type: Boolean,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index aece8ad..1d04969 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -231,6 +231,10 @@
.pointer {
cursor: pointer;
}
+ .patchset-text {
+ color: var(--deemphasized-text-color);
+ margin-left: var(--spacing-s);
+ }
</style>
<div id="container" class="container">
<div class="header" id="header" on-click="_handleToggleCollapsed">
@@ -269,7 +273,9 @@
>
<iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
</gr-button>
- <span> Patchset [[patchNum]]</span>
+ <template is="dom-if" if="[[showPatchset]]">
+ <span class="patchset-text"> Patchset [[patchNum]]</span>
+ </template>
<span class="separator"></span>
<span class="date" tabindex="0" on-click="_handleAnchorClick">
<gr-date-formatter
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 8e7ea87..717275d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -292,7 +292,8 @@
const item = this.items.find(item => item.id === id);
if (id && !this.disabledIds.includes(id)) {
if (item) {
- this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
+ this.dispatchEvent(new CustomEvent('tap-item',
+ {detail: item, bubbles: true, composed: true}));
}
this.dispatchEvent(new CustomEvent('tap-item-' + id));
}
diff --git a/polygerrit-ui/app/elements/shared/gr-event-emitter/gr-event-emitter.js b/polygerrit-ui/app/elements/shared/gr-event-emitter/gr-event-emitter.js
deleted file mode 100644
index cfe4c4f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-event-emitter/gr-event-emitter.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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.
- */
-
-import {EventEmitter} from '../gr-event-interface/gr-event-interface.js';
-
-// TODO(dmfilippov): move to appContext
-export const gerritEventEmitter = new EventEmitter();
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 85caa61..5fd7547 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -55,6 +55,8 @@
<g id="help"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+ <g id="info-outline"><path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full-->
<g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment-->
@@ -102,6 +104,8 @@
<g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#visibility-->
<g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons -->
+ <g id="schedule"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
index ef57ae9..ce65755 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
@@ -21,8 +21,8 @@
*/
import {pluginLoader} from './gr-plugin-loader.js';
-import {gerritEventEmitter} from '../gr-event-emitter/gr-event-emitter.js';
import {getRestAPI, send} from './gr-api-utils.js';
+import {appContext} from '../../../services/app-context.js';
/**
* Trigger the preinstalls for bundled plugins.
@@ -146,9 +146,11 @@
return pluginLoader.isPluginLoaded(pathOrUrl);
};
+ const eventEmitter = appContext.eventEmitter;
+
// TODO(taoalpha): List all internal supported event names.
// Also convert this to inherited class once we move Gerrit to class.
- globalGerritObj._eventEmitter = gerritEventEmitter;
+ globalGerritObj._eventEmitter = eventEmitter;
['addListener',
'dispatch',
'emit',
@@ -180,7 +182,7 @@
* });
* });
*/
- globalGerritObj[method] = gerritEventEmitter[method]
- .bind(gerritEventEmitter);
+ globalGerritObj[method] = eventEmitter[method]
+ .bind(eventEmitter);
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
index 6663f07..50a837d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
-import {gerritEventEmitter} from '../gr-event-emitter/gr-event-emitter.js';
+import {appContext} from '../../../services/app-context.js';
const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
const MAX_GET_TOKEN_RETRIES = 2;
@@ -34,6 +34,7 @@
this._status = Auth.STATUS.UNDETERMINED;
this._authCheckPromise = null;
this._last_auth_check_time = Date.now();
+ this.eventEmitter = appContext.eventEmitter;
}
get baseUrl() {
@@ -83,7 +84,7 @@
if (this._status === status) return;
if (this._status === Auth.STATUS.AUTHED) {
- gerritEventEmitter.emit('auth-error', {
+ this.eventEmitter.emit('auth-error', {
message: Auth.CREDS_EXPIRED_MSG, action: 'Refresh credentials',
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index f919cbb..f3abdb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -28,7 +28,7 @@
import '../../../test/common-test-setup.js';
import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
import {Auth, authService} from './gr-auth.js';
-import {gerritEventEmitter} from '../gr-event-emitter/gr-event-emitter.js';
+import {appContext} from '../../../services/app-context.js';
suite('gr-auth', () => {
let auth;
@@ -161,7 +161,7 @@
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 403}));
const emitStub = sinon.stub();
- gerritEventEmitter.emit = emitStub;
+ appContext.eventEmitter.emit = emitStub;
auth.authCheck().then(authed2 => {
assert.isFalse(authed2);
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
@@ -179,7 +179,7 @@
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
const emitStub = sinon.stub();
- gerritEventEmitter.emit = emitStub;
+ appContext.eventEmitter.emit = emitStub;
auth.authCheck().then(authed2 => {
assert.isFalse(authed2);
assert.isTrue(emitStub.called);
@@ -197,7 +197,7 @@
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 204}));
const emitStub = sinon.stub();
- gerritEventEmitter.emit = emitStub;
+ appContext.eventEmitter.emit = emitStub;
auth.authCheck().then(authed2 => {
assert.isTrue(authed2);
assert.isFalse(emitStub.called);
@@ -215,7 +215,7 @@
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
const emitStub = sinon.stub();
- gerritEventEmitter.emit = emitStub;
+ appContext.eventEmitter.emit = emitStub;
auth.authCheck().then(authed2 => {
assert.isFalse(authed2);
assert.isFalse(emitStub.called);
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.js b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
index 8a7fb66..90110f0 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
@@ -24,7 +24,7 @@
}
/**
- * @returns {string[]} array of all enabled experiments.
+ * @returns {!Array<string>} array of all enabled experiments.
*/
get enabledExperiments() {
return [];
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 0103bf2..e4be858 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -15,17 +15,6 @@
* limitations under the License.
*/
-function getPathFromNode(el) {
- if (!el.tagName || el.tagName === 'GR-APP'
- || el instanceof DocumentFragment
- || el instanceof HTMLSlotElement) {
- return '';
- }
- let path = el.tagName.toLowerCase();
- if (el.id) path += `#${el.id}`;
- if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
- return path;
-}
// TODO (dmfilippov): Each function must be exported separately. According to
// the code style guide, a namespacing is not allowed.
export const util = {
@@ -76,133 +65,4 @@
};
return wrappedPromise;
},
-
- /**
- * Get computed style value.
- *
- * If ShadyCSS is provided, use ShadyCSS api.
- * If `getComputedStyleValue` is provided on the element, use it.
- * Otherwise fallback to native method (in polymer 2).
- *
- */
- getComputedStyleValue: (name, el) => {
- let style;
- if (window.ShadyCSS) {
- style = ShadyCSS.getComputedStyleValue(el, name);
- } else if (el.getComputedStyleValue) {
- style = el.getComputedStyleValue(name);
- } else {
- style = getComputedStyle(el).getPropertyValue(name);
- }
- return style;
- },
-
- /**
- * Query selector on a dom element.
- *
- * This is shadow DOM compatible, but only works when selector is within
- * one shadow host, won't work if your selector is crossing
- * multiple shadow hosts.
- *
- */
- querySelector: (el, selector) => {
- let nodes = [el];
- let result = null;
- while (nodes.length) {
- const node = nodes.pop();
-
- // Skip if it's an invalid node.
- if (!node || !node.querySelector) continue;
-
- // Try find it with native querySelector directly
- result = node.querySelector(selector);
-
- if (result) {
- break;
- }
-
- // Add all nodes with shadowRoot and loop through
- const allShadowNodes = [...node.querySelectorAll('*')]
- .filter(child => !!child.shadowRoot)
- .map(child => child.shadowRoot);
- nodes = nodes.concat(allShadowNodes);
-
- // Add shadowRoot of current node if has one
- // as its not included in node.querySelectorAll('*')
- if (node.shadowRoot) {
- nodes.push(node.shadowRoot);
- }
- }
- return result;
- },
-
- /**
- * Query selector all dom elements matching with certain selector.
- *
- * This is shadow DOM compatible, but only works when selector is within
- * one shadow host, won't work if your selector is crossing
- * multiple shadow hosts.
- *
- * Note: this can be very expensive, only use when have to.
- */
- querySelectorAll: (el, selector) => {
- let nodes = [el];
- const results = new Set();
- while (nodes.length) {
- const node = nodes.pop();
-
- if (!node || !node.querySelectorAll) continue;
-
- // Try find all from regular children
- [...node.querySelectorAll(selector)]
- .forEach(el => results.add(el));
-
- // Add all nodes with shadowRoot and loop through
- const allShadowNodes = [...node.querySelectorAll('*')]
- .filter(child => !!child.shadowRoot)
- .map(child => child.shadowRoot);
- nodes = nodes.concat(allShadowNodes);
-
- // Add shadowRoot of current node if has one
- // as its not included in node.querySelectorAll('*')
- if (node.shadowRoot) {
- nodes.push(node.shadowRoot);
- }
- }
- return [...results];
- },
-
- /**
- * Retrieves the dom path of the current event.
- *
- * If the event object contains a `path` property, then use it,
- * otherwise, construct the dom path based on the event target.
- *
- * @param {!Event} e
- * @return {string}
- * @example
- *
- * domNode.onclick = e => {
- * getEventPath(e); // eg: div.class1>p#pid.class2
- * }
- */
- getEventPath: e => {
- if (!e) return '';
-
- let path = e.path;
- if (!path || !path.length) {
- path = [];
- let el = e.target;
- while (el) {
- path.push(el);
- el = el.parentNode || el.host;
- }
- }
-
- return path.reduce((domPath, curEl) => {
- const pathForEl = getPathFromNode(curEl);
- if (!pathForEl) return domPath;
- return domPath ? `${pathForEl}>${domPath}` : pathForEl;
- }, '');
- },
};
diff --git a/polygerrit-ui/app/scripts/util_test.html b/polygerrit-ui/app/scripts/util_test.html
deleted file mode 100644
index a3893d2..0000000
--- a/polygerrit-ui/app/scripts/util_test.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
- <template>
- <div id="test" class="a b c">
- <a class="testBtn"></a>
- </div>
- </template>
-</test-fixture>
-
-<script type="module">
- import '../test/common-test-setup.js';
- import {util} from './util.js';
- suite('util tests', () => {
- suite('getEventPath', () => {
- test('empty event', () => {
- assert.equal(util.getEventPath(), '');
- assert.equal(util.getEventPath(null), '');
- assert.equal(util.getEventPath(undefined), '');
- assert.equal(util.getEventPath({}), '');
- });
-
- test('event with fake path', () => {
- assert.equal(util.getEventPath({path: []}), '');
- assert.equal(util.getEventPath({path: [
- {tagName: 'dd'},
- ]}), 'dd');
- });
-
- test('event with fake complicated path', () => {
- assert.equal(util.getEventPath({path: [
- {tagName: 'dd', id: 'test', className: 'a b'},
- {tagName: 'DIV', id: 'test2', className: 'a b c'},
- ]}), 'div#test2.a.b.c>dd#test.a.b');
- });
-
- test('event with fake target', () => {
- const fakeTargetParent2 = {
- tagName: 'DIV', id: 'test2', className: 'a b c',
- };
- const fakeTargetParent1 = {
- parentNode: fakeTargetParent2,
- tagName: 'dd',
- id: 'test',
- className: 'a b',
- };
- const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
- assert.equal(
- util.getEventPath({target: fakeTarget}),
- 'div#test2.a.b.c>dd#test.a.b>span'
- );
- });
-
- test('event with real click', () => {
- const element = fixture('basic');
- const aLink = element.querySelector('a');
- let path;
- aLink.onclick = e => path = util.getEventPath(e);
- MockInteractions.click(aLink);
- assert.equal(
- path,
- 'html>body>test-fixture#basic>div#test.a.b.c>a.testBtn'
- );
- });
- });
- });
-</script>
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
index 983314e..fa6a44bf 100644
--- a/polygerrit-ui/app/services/app-context-init.js
+++ b/polygerrit-ui/app/services/app-context-init.js
@@ -17,6 +17,7 @@
import {appContext} from './app-context.js';
import {FlagsService} from './flags.js';
import {GrReporting} from './gr-reporting/gr-reporting.js';
+import {EventEmitter} from './gr-event-interface/gr-event-interface.js';
const initializedServices = new Map();
@@ -46,6 +47,6 @@
addService('flagsService', () => new FlagsService());
addService('reportingService',
() => new GrReporting(appContext.flagsService));
-
+ addService('eventEmitter', () => new EventEmitter());
Object.defineProperties(appContext, registeredServices);
}
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.js
index 62b396d..d82750e 100644
--- a/polygerrit-ui/app/services/app-context.js
+++ b/polygerrit-ui/app/services/app-context.js
@@ -24,4 +24,5 @@
export const appContext = {
flagsService: null,
reportingService: null,
+ eventEmitter: null,
};
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js
similarity index 100%
rename from polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js
rename to polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.html
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
rename to polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.html
index 74936ad..ef2c539 100644
--- a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.html
@@ -30,10 +30,10 @@
</test-fixture>
<script type="module">
-import '../../../test/common-test-setup.js';
-import '../gr-js-api-interface/gr-js-api-interface.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-js-api-interface/gr-js-api-interface.js';
import {EventEmitter} from './gr-event-interface.js';
-import {_testOnly_initGerritPluginApi} from '../gr-js-api-interface/gr-gerrit.js';
+import {_testOnly_initGerritPluginApi} from '../../elements/shared/gr-js-api-interface/gr-gerrit.js';
const pluginApi = _testOnly_initGerritPluginApi();
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index db0d279..5131b9d 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -91,16 +91,6 @@
assert.equal(cleanups.length, 0);
_testOnly_resetPluginLoader();
-
- initAppContext();
- function setMock(serviceName, setupMock) {
- Object.defineProperty(appContext, serviceName, {
- get() {
- return setupMock;
- },
- });
- }
- setMock('reportingService', grReportingMock);
});
if (isKarmaTest() || window.stub) {
@@ -125,6 +115,16 @@
throw new Error('window.stub must be set after wct sets it');
}
+initAppContext();
+function setMock(serviceName, setupMock) {
+ Object.defineProperty(appContext, serviceName, {
+ get() {
+ return setupMock;
+ },
+ });
+}
+setMock('reportingService', grReportingMock);
+
teardown(() => {
// WCT incorrectly uses teardown method in the 'fixture' and 'stub'
// implementations. This leads to slowdown WCT tests after each tests.
diff --git a/polygerrit-ui/app/test/tests.js b/polygerrit-ui/app/test/tests.js
index ad54fc3..2d795ec 100644
--- a/polygerrit-ui/app/test/tests.js
+++ b/polygerrit-ui/app/test/tests.js
@@ -41,7 +41,6 @@
'admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html',
'admin/gr-plugin-list/gr-plugin-list_test.html',
'admin/gr-repo-access/gr-repo-access_test.html',
- 'admin/gr-repo-command/gr-repo-command_test.html',
'admin/gr-repo-commands/gr-repo-commands_test.html',
'admin/gr-repo-dashboards/gr-repo-dashboards_test.html',
'admin/gr-repo-detail-list/gr-repo-detail-list_test.html',
@@ -147,7 +146,6 @@
'settings/gr-settings-view/gr-settings-view_test.html',
'settings/gr-ssh-editor/gr-ssh-editor_test.html',
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
- 'shared/gr-event-interface/gr-event-interface_test.html',
'shared/gr-account-entry/gr-account-entry_test.html',
'shared/gr-account-label/gr-account-label_test.html',
'shared/gr-account-list/gr-account-list_test.html',
@@ -245,7 +243,6 @@
'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
'gr-display-name-utils/gr-display-name-utils_test.html',
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
- 'util_test.html',
];
/* eslint-enable max-len */
for (let file of scripts) {
@@ -258,6 +255,7 @@
'flags_test.html',
'gr-reporting/gr-reporting_test.html',
'gr-reporting/gr-reporting_mock_test.html',
+ 'gr-event-interface/gr-event-interface_test.html',
];
for (let file of services) {
file = servicesPath + file;
diff --git a/polygerrit-ui/app/utils/dom-util.js b/polygerrit-ui/app/utils/dom-util.js
new file mode 100644
index 0000000..a9f080f
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util.js
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function getPathFromNode(el) {
+ if (!el.tagName || el.tagName === 'GR-APP'
+ || el instanceof DocumentFragment
+ || el instanceof HTMLSlotElement) {
+ return '';
+ }
+ let path = el.tagName.toLowerCase();
+ if (el.id) path += `#${el.id}`;
+ if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
+ return path;
+}
+
+/**
+ * Get computed style value.
+ *
+ * If ShadyCSS is provided, use ShadyCSS api.
+ * If `getComputedStyleValue` is provided on the element, use it.
+ * Otherwise fallback to native method (in polymer 2).
+ *
+ */
+export function getComputedStyleValue(name, el) {
+ let style;
+ if (window.ShadyCSS) {
+ style = ShadyCSS.getComputedStyleValue(el, name);
+ } else if (el.getComputedStyleValue) {
+ style = el.getComputedStyleValue(name);
+ } else {
+ style = getComputedStyle(el).getPropertyValue(name);
+ }
+ return style;
+}
+
+/**
+ * Query selector on a dom element.
+ *
+ * This is shadow DOM compatible, but only works when selector is within
+ * one shadow host, won't work if your selector is crossing
+ * multiple shadow hosts.
+ *
+ */
+export function querySelector(el, selector) {
+ let nodes = [el];
+ let result = null;
+ while (nodes.length) {
+ const node = nodes.pop();
+
+ // Skip if it's an invalid node.
+ if (!node || !node.querySelector) continue;
+
+ // Try find it with native querySelector directly
+ result = node.querySelector(selector);
+
+ if (result) {
+ break;
+ }
+
+ // Add all nodes with shadowRoot and loop through
+ const allShadowNodes = [...node.querySelectorAll('*')]
+ .filter(child => !!child.shadowRoot)
+ .map(child => child.shadowRoot);
+ nodes = nodes.concat(allShadowNodes);
+
+ // Add shadowRoot of current node if has one
+ // as its not included in node.querySelectorAll('*')
+ if (node.shadowRoot) {
+ nodes.push(node.shadowRoot);
+ }
+ }
+ return result;
+}
+
+/**
+ * Query selector all dom elements matching with certain selector.
+ *
+ * This is shadow DOM compatible, but only works when selector is within
+ * one shadow host, won't work if your selector is crossing
+ * multiple shadow hosts.
+ *
+ * Note: this can be very expensive, only use when have to.
+ */
+export function querySelectorAll(el, selector) {
+ let nodes = [el];
+ const results = new Set();
+ while (nodes.length) {
+ const node = nodes.pop();
+
+ if (!node || !node.querySelectorAll) continue;
+
+ // Try find all from regular children
+ [...node.querySelectorAll(selector)]
+ .forEach(el => results.add(el));
+
+ // Add all nodes with shadowRoot and loop through
+ const allShadowNodes = [...node.querySelectorAll('*')]
+ .filter(child => !!child.shadowRoot)
+ .map(child => child.shadowRoot);
+ nodes = nodes.concat(allShadowNodes);
+
+ // Add shadowRoot of current node if has one
+ // as its not included in node.querySelectorAll('*')
+ if (node.shadowRoot) {
+ nodes.push(node.shadowRoot);
+ }
+ }
+ return [...results];
+}
+
+/**
+ * Retrieves the dom path of the current event.
+ *
+ * If the event object contains a `path` property, then use it,
+ * otherwise, construct the dom path based on the event target.
+ *
+ * @param {!Event} e
+ * @return {string}
+ * @example
+ *
+ * domNode.onclick = e => {
+ * getEventPath(e); // eg: div.class1>p#pid.class2
+ * }
+ */
+export function getEventPath(e) {
+ if (!e) return '';
+
+ let path = e.path;
+ if (!path || !path.length) {
+ path = [];
+ let el = e.target;
+ while (el) {
+ path.push(el);
+ el = el.parentNode || el.host;
+ }
+ }
+
+ return path.reduce((domPath, curEl) => {
+ const pathForEl = getPathFromNode(curEl);
+ if (!pathForEl) return domPath;
+ return domPath ? `${pathForEl}>${domPath}` : pathForEl;
+ }, '');
+}
+
+/**
+ * Are any ancestors of the element (or the element itself) members of the
+ * given class.
+ *
+ * @param {!Element} element
+ * @param {string} className
+ * @param {Element=} opt_stopElement If provided, stop traversing the
+ * ancestry when the stop element is reached. The stop element's class
+ * is not checked.
+ * @return {boolean}
+ */
+export function descendedFromClass(element, className, opt_stopElement) {
+ let isDescendant = element.classList.contains(className);
+ while (!isDescendant && element.parentElement &&
+ (!opt_stopElement || element.parentElement !== opt_stopElement)) {
+ isDescendant = element.classList.contains(className);
+ element = element.parentElement;
+ }
+ return isDescendant;
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/dom-util_test.js b/polygerrit-ui/app/utils/dom-util_test.js
new file mode 100644
index 0000000..c317578
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util_test.js
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+import '../test/common-test-setup-karma.js';
+import {getComputedStyleValue, querySelector, querySelectorAll, descendedFromClass, getEventPath} from './dom-util.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+class TestEle extends PolymerElement {
+ static get is() {
+ return 'dom-util-test-element';
+ }
+
+ static get template() {
+ return html`
+ <div>
+ <div class="a">
+ <div class="b">
+ <div class="c"></div>
+ </div>
+ <span class="ss"></span>
+ </div>
+ <span class="ss"></span>
+ </div>
+ `;
+ }
+}
+
+customElements.define(TestEle.is, TestEle);
+
+const basicFixture = fixtureFromTemplate(html`
+ <div id="test" class="a b c">
+ <a class="testBtn" style="color:red;"></a>
+ <dom-util-test-element></dom-util-test-element>
+ <span class="ss"></span>
+ </div>
+`);
+
+suite('dom-util tests', () => {
+ suite('getEventPath', () => {
+ test('empty event', () => {
+ assert.equal(getEventPath(), '');
+ assert.equal(getEventPath(null), '');
+ assert.equal(getEventPath(undefined), '');
+ assert.equal(getEventPath({}), '');
+ });
+
+ test('event with fake path', () => {
+ assert.equal(getEventPath({path: []}), '');
+ assert.equal(getEventPath({path: [
+ {tagName: 'dd'},
+ ]}), 'dd');
+ });
+
+ test('event with fake complicated path', () => {
+ assert.equal(getEventPath({path: [
+ {tagName: 'dd', id: 'test', className: 'a b'},
+ {tagName: 'DIV', id: 'test2', className: 'a b c'},
+ ]}), 'div#test2.a.b.c>dd#test.a.b');
+ });
+
+ test('event with fake target', () => {
+ const fakeTargetParent2 = {
+ tagName: 'DIV', id: 'test2', className: 'a b c',
+ };
+ const fakeTargetParent1 = {
+ parentNode: fakeTargetParent2,
+ tagName: 'dd',
+ id: 'test',
+ className: 'a b',
+ };
+ const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
+ assert.equal(
+ getEventPath({target: fakeTarget}),
+ 'div#test2.a.b.c>dd#test.a.b>span'
+ );
+ });
+
+ test('event with real click', () => {
+ const element = basicFixture.instantiate();
+ const aLink = element.querySelector('a');
+ let path;
+ aLink.onclick = e => path = getEventPath(e);
+ MockInteractions.click(aLink);
+ assert.equal(
+ path,
+ `html>body>test-fixture#${basicFixture.fixtureId}>` +
+ 'div#test.a.b.c>a.testBtn'
+ );
+ });
+ });
+
+ suite('querySelector and querySelectorAll', () => {
+ test('query cross shadow dom', () => {
+ const element = basicFixture.instantiate();
+ const theFirstEl = querySelector(element, '.ss');
+ const allEls = querySelectorAll(element, '.ss');
+ assert.equal(allEls.length, 3);
+ assert.equal(theFirstEl, allEls[0]);
+ });
+ });
+
+ suite('getComputedStyleValue', () => {
+ test('color style', () => {
+ const element = basicFixture.instantiate();
+ const testBtn = querySelector(element, '.testBtn');
+ assert.equal(
+ getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)'
+ );
+ });
+ });
+
+ suite('descendedFromClass', () => {
+ test('basic tests', () => {
+ const element = basicFixture.instantiate();
+ const testEl = querySelector(element, 'dom-util-test-element');
+ // .c is a child of .a and not vice versa.
+ assert.isTrue(descendedFromClass(querySelector(testEl, '.c'), 'a'));
+ assert.isFalse(descendedFromClass(querySelector(testEl, '.a'), 'c'));
+
+ // Stops at stop element.
+ assert.isFalse(descendedFromClass(querySelector(testEl, '.c'), 'a',
+ querySelector(testEl, '.b')));
+ });
+ });
+});
\ No newline at end of file
diff --git a/resources/com/google/gerrit/httpd/raw/HostPage.html b/resources/com/google/gerrit/httpd/raw/HostPage.html
deleted file mode 100644
index c0d8446..0000000
--- a/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<html>
- <head>
- <title>Gerrit Code Review</title>
- <meta name="gwt:property" content="locale=en_US" />
- <script id="gerrit_hostpagedata"></script>
- <style id="gerrit_sitecss" type="text/css"></style>
- <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
- </head>
- <body>
- <div id="gerrit_topmenu"></div>
- <div id="gerrit_header"></div>
- <div id="gerrit_startinggerrit" style="margin-left: 10px;">
- <p>Loading <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a> ...</p>
- <noscript>
- <p>Gerrit requires a JavaScript enabled browser.</p>
- </noscript>
- </div>
- <div id="gerrit_body"></div>
- <div style="clear: both">
- <div id="gerrit_footer"></div>
- <div id="gerrit_btmmenu"></div>
- </div>
- <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
- <script id="gerrit_module"></script>
- </body>
-</html>
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 2f5447b..9908ee8 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -189,7 +189,6 @@
"repository": attr.string(default = MAVEN_CENTRAL),
"sha1": attr.string(),
"src_sha1": attr.string(),
- "unsign": attr.bool(default = False),
"exports": attr.string_list(),
"deps": attr.string_list(),
"_download_script": attr.label(default = Label("//tools:download_file.py")),
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index d49e700..ce5d62d 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -2,6 +2,8 @@
load("//tools/bzl:genrule2.bzl", "genrule2")
load("//:version.bzl", "GERRIT_VERSION")
+IN_TREE_BUILD_MODE = True
+
PLUGIN_DEPS = ["//plugins:plugin-lib"]
PLUGIN_DEPS_NEVERLINK = ["//plugins:plugin-lib-neverlink"]
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 8748250..14c726e 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -29,9 +29,6 @@
<name>Ben Rohlfs</name>
</developer>
<developer>
- <name>Dave Borowitz</name>
- </developer>
- <developer>
<name>David Ostrovsky</name>
</developer>
<developer>
@@ -53,6 +50,9 @@
<name>Martin Fick</name>
</developer>
<developer>
+ <name>Matthias Sohn</name>
+ </developer>
+ <developer>
<name>Ole Rehmsen</name>
</developer>
<developer>
@@ -61,6 +61,9 @@
<developer>
<name>Saša Živkov</name>
</developer>
+ <developer>
+ <name>Sven Selberg</name>
+ </developer>
</developers>
<mailingLists>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index ae31ac9..bd323ba 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -29,9 +29,6 @@
<name>Ben Rohlfs</name>
</developer>
<developer>
- <name>Dave Borowitz</name>
- </developer>
- <developer>
<name>David Ostrovsky</name>
</developer>
<developer>
@@ -53,6 +50,9 @@
<name>Martin Fick</name>
</developer>
<developer>
+ <name>Matthias Sohn</name>
+ </developer>
+ <developer>
<name>Ole Rehmsen</name>
</developer>
<developer>
@@ -61,6 +61,9 @@
<developer>
<name>Saša Živkov</name>
</developer>
+ <developer>
+ <name>Sven Selberg</name>
+ </developer>
</developers>
<mailingLists>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index dc25c80..3b059e5 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -29,9 +29,6 @@
<name>Ben Rohlfs</name>
</developer>
<developer>
- <name>Dave Borowitz</name>
- </developer>
- <developer>
<name>David Ostrovsky</name>
</developer>
<developer>
@@ -53,6 +50,9 @@
<name>Martin Fick</name>
</developer>
<developer>
+ <name>Matthias Sohn</name>
+ </developer>
+ <developer>
<name>Ole Rehmsen</name>
</developer>
<developer>
@@ -61,6 +61,9 @@
<developer>
<name>Saša Živkov</name>
</developer>
+ <developer>
+ <name>Sven Selberg</name>
+ </developer>
</developers>
<mailingLists>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index d21f88c..b8fa132 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -29,9 +29,6 @@
<name>Ben Rohlfs</name>
</developer>
<developer>
- <name>Dave Borowitz</name>
- </developer>
- <developer>
<name>David Ostrovsky</name>
</developer>
<developer>
@@ -53,6 +50,9 @@
<name>Martin Fick</name>
</developer>
<developer>
+ <name>Matthias Sohn</name>
+ </developer>
+ <developer>
<name>Ole Rehmsen</name>
</developer>
<developer>
@@ -61,6 +61,9 @@
<developer>
<name>Saša Živkov</name>
</developer>
+ <developer>
+ <name>Sven Selberg</name>
+ </developer>
</developers>
<mailingLists>
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 1da5004..26dcd31 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -96,8 +96,8 @@
# and httpasyncclient as necessary.
maven_jar(
name = "elasticsearch-rest-client",
- artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.7.1",
- sha1 = "6d44a8e35c11df6883747200bcf46f476a1782b8",
+ artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.8.0",
+ sha1 = "ab28f6110bdc7d2ec886e1d6ff29a6c8ee30b883",
)
maven_jar(