Merge "Disable hover style for buttons on mobile devices"
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 86221eb..628a0ec 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -19,7 +19,7 @@
Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios
implementation easy even without any Scala knowledge. The
-link:https://gitenterprise.me/2019/12/20/stress-your-gerrit-with-gatling/[Stress your Gerrit with Gatling]
+link:https://gitenterprise.me/2019/12/20/stress-your-gerrit-with-gatling/[Stress your Gerrit with Gatling,role=external,window=_blank]
blog post has more introductory information.
Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory
diff --git a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
index 9e2aca0..c5a7cba 100644
--- a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
+++ b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
@@ -14,29 +14,12 @@
package com.google.gerrit.scenarios
-import java.io._
-
-import com.github.barbasa.gatling.git.protocol.GitProtocol
-import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
-import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
import io.gatling.core.Predef._
-import io.gatling.core.feeder.FileBasedFeederBuilder
import io.gatling.core.structure.ScenarioBuilder
-import org.apache.commons.io.FileUtils
-import org.eclipse.jgit.hooks._
import scala.concurrent.duration._
-class CloneUsingBothProtocols extends Simulation {
-
- implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
- implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
-
- private val name: String = this.getClass.getSimpleName
- private val file = s"data/$name.json"
- private val data: FileBasedFeederBuilder[Any]#F = jsonFile(file).circular
- private val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
- private val protocol: GitProtocol = GitProtocol()
+class CloneUsingBothProtocols extends GitSimulation {
private val test: ScenarioBuilder = scenario(name)
.feed(data)
@@ -46,16 +29,4 @@
test.inject(
constantUsersPerSec(1) during (2 seconds)
)).protocols(protocol)
-
- after {
- Thread.sleep(5000)
- val path = conf.tmpBasePath
- try {
- FileUtils.deleteDirectory(new File(path))
- } catch {
- case e: IOException =>
- System.err.println("Unable to delete temporary directory " + path)
- e.printStackTrace()
- }
- }
}
diff --git a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
new file mode 100644
index 0000000..4d5130f
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
@@ -0,0 +1,48 @@
+// 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.scenarios
+
+import java.io.{File, IOException}
+
+import com.github.barbasa.gatling.git.protocol.GitProtocol
+import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
+import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
+import org.apache.commons.io.FileUtils
+import org.eclipse.jgit.hooks.CommitMsgHook
+
+class GitSimulation extends Simulation {
+
+ implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
+ implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
+
+ protected val name: String = this.getClass.getSimpleName
+ protected val data: FileBasedFeederBuilder[Any]#F = jsonFile(s"data/$name.json").circular
+ protected val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
+ protected val protocol: GitProtocol = GitProtocol()
+
+ after {
+ Thread.sleep(5000)
+ val path = conf.tmpBasePath
+ try {
+ FileUtils.deleteDirectory(new File(path))
+ } catch {
+ case e: IOException =>
+ System.err.println("Unable to delete temporary directory " + path)
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
index 5a3bb99..82342be 100644
--- a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
+++ b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
@@ -14,29 +14,12 @@
package com.google.gerrit.scenarios
-import java.io._
-
-import com.github.barbasa.gatling.git.protocol.GitProtocol
-import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
-import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
import io.gatling.core.Predef._
-import io.gatling.core.feeder.FileBasedFeederBuilder
import io.gatling.core.structure.ScenarioBuilder
-import org.apache.commons.io.FileUtils
-import org.eclipse.jgit.hooks._
import scala.concurrent.duration._
-class ReplayRecordsFromFeeder extends Simulation {
-
- implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
- implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
-
- private val name: String = this.getClass.getSimpleName
- private val file = s"data/$name.json"
- private val data: FileBasedFeederBuilder[Any]#F = jsonFile(file).circular
- private val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
- private val protocol: GitProtocol = GitProtocol()
+class ReplayRecordsFromFeeder extends GitSimulation {
private val test: ScenarioBuilder = scenario(name)
.repeat(10000) {
@@ -53,16 +36,4 @@
constantUsersPerSec(20) during (15 seconds) randomized
)).protocols(protocol)
.maxDuration(60 seconds)
-
- after {
- Thread.sleep(5000)
- val path = conf.tmpBasePath
- try {
- FileUtils.deleteDirectory(new File(path))
- } catch {
- case e: IOException =>
- System.err.println("Unable to delete temporary directory " + path)
- e.printStackTrace()
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/client/ListOption.java b/java/com/google/gerrit/extensions/client/ListOption.java
index e694c0e..4dea42f 100644
--- a/java/com/google/gerrit/extensions/client/ListOption.java
+++ b/java/com/google/gerrit/extensions/client/ListOption.java
@@ -16,6 +16,7 @@
import java.lang.reflect.InvocationTargetException;
import java.util.EnumSet;
+import java.util.Set;
/** Enum that can be expressed as a bitset in query parameters. */
public interface ListOption {
@@ -46,4 +47,13 @@
}
return r;
}
+
+ static String toHex(Set<ListChangesOption> options) {
+ int v = 0;
+ for (ListChangesOption option : options) {
+ v |= 1 << option.getValue();
+ }
+
+ return Integer.toHexString(v);
+ }
}
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index b1d4ac6..6a66ba3 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -18,15 +18,20 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.json.OutputFormat;
import com.google.gson.Gson;
import com.google.template.soy.data.SanitizedContent;
@@ -34,11 +39,46 @@
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/** Helper for generating parts of {@code index.html}. */
public class IndexHtmlUtil {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ static final String changeCanonicalUrl = ".*/c/(?<project>.+)/\\+/(?<changeNum>\\d+)";
+ static final String basePatchNumUrlPart = "(/(-?\\d+|edit)(\\.\\.(\\d+|edit))?)";
+ static final Pattern changeUrlPattern =
+ Pattern.compile(changeCanonicalUrl + basePatchNumUrlPart + "?" + "/?$");
+ static final Pattern diffUrlPattern =
+ Pattern.compile(changeCanonicalUrl + basePatchNumUrlPart + "(/(.+))" + "/?$");
+
+ public static String getDefaultChangeDetailHex() {
+ Set<ListChangesOption> options =
+ ImmutableSet.of(
+ ListChangesOption.ALL_COMMITS,
+ ListChangesOption.ALL_REVISIONS,
+ ListChangesOption.CHANGE_ACTIONS,
+ ListChangesOption.DETAILED_LABELS,
+ ListChangesOption.DOWNLOAD_COMMANDS,
+ ListChangesOption.MESSAGES,
+ ListChangesOption.SUBMITTABLE,
+ ListChangesOption.WEB_LINKS,
+ ListChangesOption.SKIP_DIFFSTAT);
+
+ return ListOption.toHex(options);
+ }
+
+ public static String getDefaultDiffDetailHex() {
+ Set<ListChangesOption> options =
+ ImmutableSet.of(
+ ListChangesOption.ALL_COMMITS,
+ ListChangesOption.ALL_REVISIONS,
+ ListChangesOption.SKIP_DIFFSTAT);
+
+ return ListOption.toHex(options);
+ }
/**
* Returns both static and dynamic parameters of {@code index.html}. The result is to be used when
@@ -50,12 +90,18 @@
String cdnPath,
String faviconPath,
Map<String, String[]> urlParameterMap,
- Function<String, SanitizedContent> urlInScriptTagOrdainer)
+ Function<String, SanitizedContent> urlInScriptTagOrdainer,
+ String requestedURL)
throws URISyntaxException, RestApiException {
return ImmutableMap.<String, Object>builder()
.putAll(
staticTemplateData(
- canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
+ canonicalURL,
+ cdnPath,
+ faviconPath,
+ urlParameterMap,
+ urlInScriptTagOrdainer,
+ requestedURL))
.putAll(dynamicTemplateData(gerritApi))
.build();
}
@@ -98,7 +144,8 @@
String cdnPath,
String faviconPath,
Map<String, String[]> urlParameterMap,
- Function<String, SanitizedContent> urlInScriptTagOrdainer)
+ Function<String, SanitizedContent> urlInScriptTagOrdainer,
+ String requestedURL)
throws URISyntaxException {
String canonicalPath = computeCanonicalPath(canonicalURL);
@@ -133,9 +180,39 @@
if (urlParameterMap.containsKey("gf")) {
data.put("useGoogleFonts", "true");
}
+
+ if (urlParameterMap.containsKey("pl") && requestedURL != null) {
+ data.put("defaultChangeDetailHex", getDefaultChangeDetailHex());
+ data.put("defaultDiffDetailHex", getDefaultDiffDetailHex());
+
+ String changeRequestsPath = computeChangeRequestsPath(requestedURL, changeUrlPattern);
+ if (changeRequestsPath != null) {
+ data.put("preloadChangePage", "true");
+ } else {
+ changeRequestsPath = computeChangeRequestsPath(requestedURL, diffUrlPattern);
+ data.put("preloadDiffPage", "true");
+ }
+
+ if (changeRequestsPath != null) {
+ data.put("changeRequestsPath", changeRequestsPath);
+ }
+ }
+
return data.build();
}
+ static String computeChangeRequestsPath(String requestedURL, Pattern pattern) {
+ Matcher matcher = pattern.matcher(requestedURL);
+ if (matcher.matches()) {
+ Integer changeId = Ints.tryParse(matcher.group("changeNum"));
+ if (changeId != null) {
+ return "changes/" + Url.encode(matcher.group("project")) + "~" + changeId;
+ }
+ }
+
+ return null;
+ }
+
private static String computeCanonicalPath(@Nullable String canonicalURL)
throws URISyntaxException {
if (Strings.isNullOrEmpty(canonicalURL)) {
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index a0b41b21..97d2270 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -70,10 +70,11 @@
SoySauce.Renderer renderer;
try {
Map<String, String[]> parameterMap = req.getParameterMap();
+ String requestUrl = req.getRequestURL() == null ? null : req.getRequestURL().toString();
// TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
ImmutableMap<String, Object> templateData =
IndexHtmlUtil.templateData(
- gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer);
+ gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer, requestUrl);
renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
} catch (URISyntaxException | RestApiException e) {
throw new IOException(e);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ae5066b..0eefe02 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4637,7 +4637,6 @@
ListChangesOption.ALL_COMMITS,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.CURRENT_ACTIONS,
ListChangesOption.DETAILED_LABELS,
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index d9438f0..cad4796 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -15,6 +15,9 @@
package com.google.gerrit.httpd.raw;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.changeUrlPattern;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.computeChangeRequestsPath;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.diffUrlPattern;
import static com.google.gerrit.httpd.raw.IndexHtmlUtil.staticTemplateData;
import com.google.template.soy.data.SanitizedContent;
@@ -29,7 +32,12 @@
public void noPathAndNoCDN() throws Exception {
assertThat(
staticTemplateData(
- "http://example.com/", null, null, new HashMap<>(), IndexHtmlUtilTest::ordain))
+ "http://example.com/",
+ null,
+ null,
+ new HashMap<>(),
+ IndexHtmlUtilTest::ordain,
+ null))
.containsExactly("canonicalPath", "", "staticResourcePath", ordain(""));
}
@@ -41,7 +49,8 @@
null,
null,
new HashMap<>(),
- IndexHtmlUtilTest::ordain))
+ IndexHtmlUtilTest::ordain,
+ null))
.containsExactly("canonicalPath", "/gerrit", "staticResourcePath", ordain("/gerrit"));
}
@@ -53,7 +62,8 @@
"http://my-cdn.com/foo/bar/",
null,
new HashMap<>(),
- IndexHtmlUtilTest::ordain))
+ IndexHtmlUtilTest::ordain,
+ null))
.containsExactly(
"canonicalPath", "", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
}
@@ -66,7 +76,8 @@
"http://my-cdn.com/foo/bar/",
null,
new HashMap<>(),
- IndexHtmlUtilTest::ordain))
+ IndexHtmlUtilTest::ordain,
+ null))
.containsExactly(
"canonicalPath", "/gerrit", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
}
@@ -77,11 +88,51 @@
urlParms.put("gf", new String[0]);
assertThat(
staticTemplateData(
- "http://example.com/", null, null, urlParms, IndexHtmlUtilTest::ordain))
+ "http://example.com/", null, null, urlParms, IndexHtmlUtilTest::ordain, null))
.containsExactly(
"canonicalPath", "", "staticResourcePath", ordain(""), "useGoogleFonts", "true");
}
+ @Test
+ public void usePreloadRest() throws Exception {
+ Map<String, String[]> urlParms = new HashMap<>();
+ urlParms.put("pl", new String[0]);
+ assertThat(
+ staticTemplateData(
+ "http://example.com/",
+ null,
+ null,
+ urlParms,
+ IndexHtmlUtilTest::ordain,
+ "/c/project/+/123"))
+ .containsExactly(
+ "canonicalPath", "",
+ "staticResourcePath", ordain(""),
+ "defaultChangeDetailHex", "916314",
+ "defaultDiffDetailHex", "800014",
+ "preloadChangePage", "true",
+ "changeRequestsPath", "changes/project~123");
+ }
+
+ @Test
+ public void computeChangePath() throws Exception {
+ assertThat(computeChangeRequestsPath("/c/project/+/123", changeUrlPattern))
+ .isEqualTo("changes/project~123");
+
+ assertThat(computeChangeRequestsPath("/c/project/+/124/2", changeUrlPattern))
+ .isEqualTo("changes/project~124");
+
+ assertThat(computeChangeRequestsPath("/c/project/src/+/23", changeUrlPattern))
+ .isEqualTo("changes/project%2Fsrc~23");
+
+ assertThat(computeChangeRequestsPath("/q/project/src/+/23", changeUrlPattern)).isEqualTo(null);
+
+ assertThat(computeChangeRequestsPath("/c/Scripts/+/232/1//COMMIT_MSG", changeUrlPattern))
+ .isEqualTo(null);
+ assertThat(computeChangeRequestsPath("/c/Scripts/+/232/1//COMMIT_MSG", diffUrlPattern))
+ .isEqualTo("changes/Scripts~232");
+ }
+
private static SanitizedContent ordain(String s) {
return UnsafeSanitizedContentOrdainer.ordainAsSafe(
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index 8a1ed87..495f8ab 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -148,6 +148,7 @@
NEXT_LINE: 'NEXT_LINE',
PREV_LINE: 'PREV_LINE',
+ VISIBLE_LINE: 'VISIBLE_LINE',
NEXT_CHUNK: 'NEXT_CHUNK',
PREV_CHUNK: 'PREV_CHUNK',
EXPAND_ALL_DIFF_CONTEXT: 'EXPAND_ALL_DIFF_CONTEXT',
@@ -237,6 +238,8 @@
_describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line');
_describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line');
+ _describe(Shortcut.VISIBLE_LINE, ShortcutSection.DIFFS,
+ 'Move cursor to currently visible code');
_describe(Shortcut.NEXT_CHUNK, ShortcutSection.DIFFS,
'Go to next diff chunk');
_describe(Shortcut.PREV_CHUNK, ShortcutSection.DIFFS,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 05a6209f..7674099 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -299,11 +299,10 @@
class="action save"
has-tooltip
title="[[_saveTooltip]]"
- on-click="_saveClickHandler">Send</gr-button>
+ on-click="_saveClickHandler">Save</gr-button>
</template>
<gr-button
id="sendButton"
- link
primary
disabled="[[_sendDisabled]]"
class="action send"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 90f8540..fac631b 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -43,7 +43,7 @@
};
const ButtonTooltips = {
- SAVE: 'Send but do not send notification or change review state',
+ SAVE: 'Save but do not send notification or change review state',
START_REVIEW: 'Mark as ready for review and send reply',
SEND: 'Send reply',
};
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 05bff9f..7a20dbb 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
@@ -248,14 +248,12 @@
if (showPartialLinks) {
td.appendChild(this._createContextButton(
GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
- td.appendChild(document.createTextNode(' - '));
}
td.appendChild(this._createContextButton(
GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
if (showPartialLinks) {
- td.appendChild(document.createTextNode(' - '));
td.appendChild(this._createContextButton(
GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
}
@@ -273,22 +271,26 @@
let text;
let groups = []; // The groups that replace this one if tapped.
-
if (type === GrDiffBuilder.ContextButtonType.ALL) {
+ const icon = this._createElement('iron-icon', 'showContext');
+ icon.setAttribute('icon', 'gr-icons:unfold-more');
+ Polymer.dom(button).appendChild(icon);
+
text = 'Show ' + numLines + ' common line';
if (numLines > 1) { text += 's'; }
groups.push(...line.contextGroups);
} else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
- text = '+' + context + '↑';
+ text = '+' + context + ' above';
groups = GrDiffGroup.hideInContextControl(line.contextGroups,
context, numLines);
} else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
- text = '+' + context + '↓';
+ text = '+' + context + ' below';
groups = GrDiffGroup.hideInContextControl(line.contextGroups,
0, numLines - context);
}
-
- Polymer.dom(button).textContent = text;
+ const textSpan = this._createElement('span', 'showContext');
+ Polymer.dom(textSpan).textContent = text;
+ Polymer.dom(button).appendChild(textSpan);
button.addEventListener('tap', e => {
e.detail = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 589d7c2..320909c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -125,9 +125,9 @@
buttons = td.querySelectorAll('gr-button.showContext');
assert.equal(buttons.length, 3);
- assert.equal(Polymer.dom(buttons[0]).textContent, '+10↑');
+ assert.equal(Polymer.dom(buttons[0]).textContent, '+10 above');
assert.equal(Polymer.dom(buttons[1]).textContent, 'Show 11 common lines');
- assert.equal(Polymer.dom(buttons[2]).textContent, '+10↓');
+ assert.equal(Polymer.dom(buttons[2]).textContent, '+10 below');
});
test('newlines 1', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index e8d629c..87152d8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -180,6 +180,15 @@
}
}
+ moveToVisibleArea() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.moveToVisibleArea(
+ this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.moveToVisibleArea();
+ }
+ }
+
moveToNextChunk(opt_clipToTop) {
this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
target => target.parentNode.scrollHeight, opt_clipToTop);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 2fbf59b..cb89c0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -229,6 +229,7 @@
[this.Shortcut.RIGHT_PANE]: '_handleRightPane',
[this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
[this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
[this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
'_handleNextLineOrFileWithComments',
[this.Shortcut.PREV_FILE_WITH_COMMENTS]:
@@ -388,6 +389,13 @@
this.$.cursor.moveUp();
}
+ _handleVisibleLine(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveToVisibleArea();
+ }
+
_onOpenFixPreview(e) {
this.$.applyFixDialog.open(e);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 172a151..4301ac2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -192,11 +192,18 @@
.contextControl gr-button {
display: inline-block;
text-decoration: none;
+ vertical-align: top;
+ line-height: var(--line-height-mono, 18px);
--gr-button: {
color: var(--diff-context-control-color);
- padding: var(--spacing-xs);
+ padding: var(--spacing-xxs) var(--spacing-l);
}
}
+ .contextControl gr-button iron-icon {
+ /* should match line-height of gr-button */
+ width: var(--line-height-mono, 18px);
+ height: var(--line-height-mono, 18px);
+ }
.contextControl td:not(.lineNum) {
text-align: center;
}
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index d39ba58..c1e317a 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -240,6 +240,10 @@
this.Shortcut.NEXT_LINE, 'j', 'down');
this.bindShortcut(
this.Shortcut.PREV_LINE, 'k', 'up');
+ if (this._isCursorManagerSupportMoveToVisibleLine()) {
+ this.bindShortcut(
+ this.Shortcut.VISIBLE_LINE, '.');
+ }
this.bindShortcut(
this.Shortcut.NEXT_CHUNK, 'n');
this.bindShortcut(
@@ -303,6 +307,14 @@
this.Shortcut.SEARCH, '/');
}
+ _isCursorManagerSupportMoveToVisibleLine() {
+ // This method is a copy-paste from the
+ // method _isIntersectionObserverSupported of gr-cursor-manager.js
+ // It is better share this method with gr-cursor-manager,
+ // but doing it require a lot if changes instead of 1-line copied code
+ return 'IntersectionObserver' in window;
+ }
+
_accountChanged(account) {
if (!account) { return; }
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 37c7147..4f98e88 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -131,6 +131,75 @@
}
/**
+ * Move the cursor to the row which is the closest to the viewport center
+ * in vertical direction.
+ * The method uses IntersectionObservers API. If browser
+ * doesn't support this API the method does nothing
+ *
+ * @param {!Function=} opt_condition Optional condition. If a condition
+ * is passed only stops which meet conditions are taken into account.
+ */
+ moveToVisibleArea(opt_condition) {
+ if (!this.stops || !this._isIntersectionObserverSupported()) {
+ return;
+ }
+ const filteredStops = opt_condition ? this.stops.filter(opt_condition)
+ : this.stops;
+ const dims = this._getWindowDims();
+ const windowCenter =
+ Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
+
+ let closestToTheCenter = null;
+ let minDistanceToCenter = null;
+ let unobservedCount = filteredStops.length;
+
+ const observer = new IntersectionObserver(entries => {
+ // This callback is called for the first time immediately.
+ // Typically it gets all observed stops at once, but
+ // sometimes can get them in several chunks.
+ entries.forEach(entry => {
+ observer.unobserve(entry.target);
+
+ // In Edge it is recommended to use intersectionRatio instead of
+ // isIntersecting.
+ const isInsideViewport =
+ entry.isIntersecting || entry.intersectionRatio > 0;
+ if (!isInsideViewport) {
+ return;
+ }
+ const center = entry.boundingClientRect.top + Math.round(
+ entry.boundingClientRect.height / 2);
+ const distanceToWindowCenter = Math.abs(center - windowCenter);
+ if (minDistanceToCenter === null ||
+ distanceToWindowCenter < minDistanceToCenter) {
+ closestToTheCenter = entry.target;
+ minDistanceToCenter = distanceToWindowCenter;
+ }
+ });
+ unobservedCount -= entries.length;
+ if (unobservedCount == 0 && closestToTheCenter) {
+ // set cursor when all stops were observed.
+ // In most cases the target is visible, so scroll is not
+ // needed. But in rare cases the target can become invisible
+ // at this point (due to some scrolling in window).
+ // To avoid jumps set noScroll options.
+ this.setCursor(closestToTheCenter, true);
+ }
+ });
+ filteredStops.forEach(stop => {
+ observer.observe(stop);
+ });
+ }
+
+ _isIntersectionObserverSupported() {
+ // The copy of this method exists in gr-app-element.js under the
+ // name _isCursorManagerSupportMoveToVisibleLine
+ // If you update this method, you must update gr-app-element.js
+ // as well.
+ return 'IntersectionObserver' in window;
+ }
+
+ /**
* Set the cursor to an arbitrary element.
*
* @param {!HTMLElement} element
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index 75ab77a..d36ce3b 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -24,6 +24,8 @@
<g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></g>
+ <!-- This SVG is a copy from material.io https://material.io/icons/#unfold_more -->
+ <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"/></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 68b6756..b0186f0 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -278,16 +278,58 @@
let links = element.$.output.querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+ assert.equal(links[0].innerHTML, 'mailto:test@google.com');
element.content = 'xx http://google.com yy';
links = element.$.output.querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'http://google.com');
+ assert.equal(links[0].innerHTML, 'http://google.com');
element.content = 'xx https://google.com yy';
links = element.$.output.querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ element.content = 'xx ssh://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+
+ element.content = 'xx ftp://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+ });
+
+ test('links without leading whitespace are linkified', () => {
+ element.content = 'xx abcmailto:test@google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
+ let links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+ assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+ element.content = 'xx defhttp://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'http://google.com');
+ assert.equal(links[0].innerHTML, 'http://google.com');
+
+ element.content = 'xx qwehttps://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ // Non-latin character
+ element.content = 'xx абвhttps://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
element.content = 'xx ssh://google.com yy';
links = element.$.output.querySelectorAll('a');
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index c4f427e..aac6d76 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -22,7 +22,7 @@
*
* @type {RegExp}
*/
- const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/;
+ const URL_PROTOCOL_PATTERN = /^(.*)(https?:\/\/|mailto:)/;
/**
* Construct a parser for linkifying text. Will linkify plain URLs that appear
@@ -257,13 +257,29 @@
// the source text does not include a protocol, the protocol will be added
// by ba-linkify. Create the link if the href is provided and its protocol
// matches the expected pattern.
- if (href && URL_PROTOCOL_PATTERN.test(href)) {
- this.addText(text, href);
- } else {
- // For the sections of text that lie between the links found by
- // ba-linkify, we search for the project-config-specified link patterns.
- this.parseLinks(text, this.linkConfig);
+ if (href) {
+ const result = URL_PROTOCOL_PATTERN.exec(href);
+ if (result) {
+ const prefixText = result[1];
+ if (prefixText.length > 0) {
+ // Fix for simple cases from
+ // https://bugs.chromium.org/p/gerrit/issues/detail?id=11697
+ // When leading whitespace is missed before link,
+ // linkify add this text before link as a schema name to href.
+ // We suppose, that prefixText just a single word
+ // before link and add this word as is, without processing
+ // any patterns in it.
+ this.parseLinks(prefixText, []);
+ text = text.substring(prefixText.length);
+ href = href.substring(prefixText.length);
+ }
+ this.addText(text, href);
+ return;
+ }
}
+ // For the sections of text that lie between the links found by
+ // ba-linkify, we search for the project-config-specified link patterns.
+ this.parseLinks(text, this.linkConfig);
};
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 4aefe46..ce26bf6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -990,6 +990,20 @@
* @param {function()=} opt_cancelCondition
*/
getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+ return this.getConfig(false).then(config => {
+ const optionsHex = this._getChangeOptionsHex(config);
+ return this._getChangeDetail(
+ changeNum, optionsHex, opt_errFn, opt_cancelCondition)
+ .then(GrReviewerUpdatesParser.parse);
+ });
+ }
+
+ _getChangeOptionsHex(config) {
+ if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
+ && !(config.receive && config.receive.enable_signed_push)) {
+ return window.DEFAULT_DETAIL_HEXES.changePage;
+ }
+
// This list MUST be kept in sync with
// ChangeIT#changeDetailsDoesNotRequireIndex
const options = [
@@ -1003,15 +1017,10 @@
this.ListChangesOption.WEB_LINKS,
this.ListChangesOption.SKIP_DIFFSTAT,
];
- return this.getConfig(false).then(config => {
- if (config.receive && config.receive.enable_signed_push) {
- options.push(this.ListChangesOption.PUSH_CERTIFICATES);
- }
- const optionsHex = this.listChangesOptionsToHex(...options);
- return this._getChangeDetail(
- changeNum, optionsHex, opt_errFn, opt_cancelCondition)
- .then(GrReviewerUpdatesParser.parse);
- });
+ if (config.receive && config.receive.enable_signed_push) {
+ options.push(this.ListChangesOption.PUSH_CERTIFICATES);
+ }
+ return this.listChangesOptionsToHex(...options);
}
/**
@@ -1020,11 +1029,16 @@
* @param {function()=} opt_cancelCondition
*/
getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
- const optionsHex = this.listChangesOptionsToHex(
- this.ListChangesOption.ALL_COMMITS,
- this.ListChangesOption.ALL_REVISIONS,
- this.ListChangesOption.SKIP_DIFFSTAT
- );
+ let optionsHex = '';
+ if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
+ optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
+ } else {
+ optionsHex = this.listChangesOptionsToHex(
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.SKIP_DIFFSTAT
+ );
+ }
return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
opt_cancelCondition);
}
@@ -1040,7 +1054,7 @@
const urlWithParams = this._restApiHelper
.urlWithParams(url, optionsHex);
const params = {O: optionsHex};
- let req = {
+ const req = {
url,
errFn: opt_errFn,
cancelCondition: opt_cancelCondition,
@@ -1048,7 +1062,6 @@
fetchOptions: this._etags.getOptions(urlWithParams),
anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
};
- req = this._restApiHelper.addAcceptJsonHeader(req);
return this._restApiHelper.fetchRawJSON(req).then(response => {
if (response && response.status === 304) {
return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
@@ -2039,12 +2052,15 @@
* @param {string|number=} opt_patchNum
* @return {!Promise<!Object>} Diff comments response.
*/
+ // We don't want to add accept header, since preloading of comments is
+ // working only without accept header.
+ const noAcceptHeader = true;
const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
changeNum,
endpoint,
patchNum: opt_patchNum,
reportEndpointAsIs: true,
- });
+ }, noAcceptHeader);
if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
return fetchComments();
@@ -2609,7 +2625,7 @@
* @param {Gerrit.ChangeFetchRequest} req
* @return {!Promise<!Object>}
*/
- _getChangeURLAndFetch(req) {
+ _getChangeURLAndFetch(req, noAcceptHeader) {
const anonymizedEndpoint = req.reportEndpointAsIs ?
req.endpoint : req.anonymizedEndpoint;
const anonymizedBaseUrl = req.patchNum ?
@@ -2622,7 +2638,7 @@
fetchOptions: req.fetchOptions,
anonymizedUrl: anonymizedEndpoint ?
(anonymizedBaseUrl + anonymizedEndpoint) : undefined,
- }));
+ }, noAcceptHeader));
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index ca4b246..ed7d29f 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -1036,16 +1036,6 @@
});
});
- test('_getChangeDetail accepts only json', () => {
- const authFetchStub = sandbox.stub(element._auth, 'fetch')
- .returns(Promise.resolve());
- const errFn = sinon.stub();
- element._getChangeDetail(123, '516714', errFn);
- assert.isTrue(authFetchStub.called);
- assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
- 'application/json');
- });
-
test('_getChangeDetail populates _projectLookup', () => {
sandbox.stub(element, 'getChangeActionURL')
.returns(Promise.resolve(''));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
index 91fef29..2c326df 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -204,9 +204,12 @@
* Same as {@link fetchRawJSON}, plus error handling.
*
* @param {Gerrit.FetchJSONRequest} req
+ * @param {boolean} noAcceptHeader - don't add default accept json header
*/
- fetchJSON(req) {
- req = this.addAcceptJsonHeader(req);
+ fetchJSON(req, noAcceptHeader) {
+ if (!noAcceptHeader) {
+ req = this.addAcceptJsonHeader(req);
+ }
return this.fetchRawJSON(req).then(response => {
if (!response) {
return;
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 5046a2a..b182309 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -28,6 +28,11 @@
{@param? polyfillSD: ?}
{@param? polyfillSC: ?}
{@param? useGoogleFonts: ?}
+ {@param? changeRequestsPath: ?}
+ {@param? defaultChangeDetailHex: ?}
+ {@param? defaultDiffDetailHex: ?}
+ {@param? preloadChangePage: ?}
+ {@param? preloadDiffPage: ?}
<!DOCTYPE html>{\n}
<html lang="en">{\n}
<meta charset="utf-8">{\n}
@@ -43,6 +48,14 @@
// Disable extra font load from paper-styles
window.polymerSkipLoadingFontRoboto = true;
window.CLOSURE_NO_DEPS = true;
+ window.DEFAULT_DETAIL_HEXES = {lb}
+ {if $defaultChangeDetailHex}
+ changePage: '{$defaultChangeDetailHex}',
+ {/if}
+ {if $defaultDiffDetailHex}
+ diffPage: '{$defaultDiffDetailHex}',
+ {/if}
+ {rb};
{if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
{if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
{if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
@@ -68,6 +81,16 @@
{else}
<link rel="icon" type="image/x-icon" href="{$canonicalPath}/favicon.ico">{\n}
{/if}
+ {if $changeRequestsPath}
+ {if $preloadChangePage and $defaultChangeDetailHex}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/detail?O={$defaultChangeDetailHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ {/if}
+ {if $preloadDiffPage and $defaultDiffDetailHex}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/detail?O={$defaultDiffDetailHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ {/if}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/robotcomments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ {/if}
// RobotoMono fonts are used in styles/fonts.css
{if $useGoogleFonts}