Merge "Add hovercard endpoint for buganizer hovercard plugin"
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index b85545a..0cb407e 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -2410,6 +2410,8 @@
metadata entries with the same name may be returned.
|`value` |optional|The metadata value.
|`description`|optional|A description of the metadata.
+|`web_links` |optional|A list of web links as
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entities.
|==========================
[[plugin-config-info]]
diff --git a/java/com/google/gerrit/acceptance/GerritServerRestSession.java b/java/com/google/gerrit/acceptance/GerritServerRestSession.java
index c2c77fe..9605f50 100644
--- a/java/com/google/gerrit/acceptance/GerritServerRestSession.java
+++ b/java/com/google/gerrit/acceptance/GerritServerRestSession.java
@@ -142,7 +142,8 @@
return execute(delete);
}
- private String getUrl(String endPoint) {
+ @Override
+ public String getUrl(String endPoint) {
return url + (account != null ? "/a" : "") + endPoint;
}
}
diff --git a/java/com/google/gerrit/acceptance/RestResponse.java b/java/com/google/gerrit/acceptance/RestResponse.java
index a9c14aa..a53c015 100644
--- a/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/java/com/google/gerrit/acceptance/RestResponse.java
@@ -103,4 +103,9 @@
assertStatus(SC_MOVED_TEMPORARILY);
assertThat(URI.create(getHeader("Location")).getPath()).isEqualTo(path);
}
+
+ public void assertTemporaryRedirectUri(String uri) throws Exception {
+ assertStatus(SC_MOVED_TEMPORARILY);
+ assertThat(getHeader("Location")).isEqualTo(uri);
+ }
}
diff --git a/java/com/google/gerrit/acceptance/RestSession.java b/java/com/google/gerrit/acceptance/RestSession.java
index 0865e31..3fefd5b 100644
--- a/java/com/google/gerrit/acceptance/RestSession.java
+++ b/java/com/google/gerrit/acceptance/RestSession.java
@@ -52,4 +52,6 @@
RestResponse delete(String endPoint) throws Exception;
RestResponse deleteWithHeaders(String endPoint, Header... headers) throws Exception;
+
+ String getUrl(String endPoint);
}
diff --git a/java/com/google/gerrit/extensions/common/MetadataInfo.java b/java/com/google/gerrit/extensions/common/MetadataInfo.java
index a2cdb98..2213191 100644
--- a/java/com/google/gerrit/extensions/common/MetadataInfo.java
+++ b/java/com/google/gerrit/extensions/common/MetadataInfo.java
@@ -16,6 +16,7 @@
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.Nullable;
+import java.util.List;
import java.util.Objects;
/**
@@ -37,18 +38,22 @@
/** A description of the metadata. May be unset. */
@Nullable public String description;
+ /** Web links. May be unset. */
+ @Nullable public List<WebLinkInfo> webLinks;
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("value", value)
.add("description", description)
+ .add("webLinks", webLinks)
.toString();
}
@Override
public int hashCode() {
- return Objects.hash(name, value, description);
+ return Objects.hash(name, value, description, webLinks);
}
@Override
@@ -57,7 +62,8 @@
MetadataInfo metadata = (MetadataInfo) o;
return Objects.equals(name, metadata.name)
&& Objects.equals(value, metadata.value)
- && Objects.equals(description, metadata.description);
+ && Objects.equals(description, metadata.description)
+ && Objects.equals(webLinks, metadata.webLinks);
}
return false;
}
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 9925a66..0c2691a 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -122,7 +122,7 @@
*/
public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type, Named name) {
binder.disableCircularProxies();
- return bind(binder, TypeLiteral.get(type));
+ return bind(binder, TypeLiteral.get(type), name);
}
/**
diff --git a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
index 2c80c9b..ca937fd 100644
--- a/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
+++ b/java/com/google/gerrit/httpd/NumericChangeIdRedirectServlet.java
@@ -16,6 +16,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.entities.Change;
@@ -85,6 +86,7 @@
if (finalSegment != null) {
path += finalSegment;
}
- UrlModule.toGerrit(path, req, rsp);
+ String queryString = Strings.emptyToNull(req.getQueryString());
+ UrlModule.toGerrit(path + (queryString != null ? "?" + queryString : ""), req, rsp);
}
}
diff --git a/java/com/google/gerrit/httpd/UrlModule.java b/java/com/google/gerrit/httpd/UrlModule.java
index aedf8e9..f62fbe8 100644
--- a/java/com/google/gerrit/httpd/UrlModule.java
+++ b/java/com/google/gerrit/httpd/UrlModule.java
@@ -45,6 +45,9 @@
class UrlModule extends ServletModule {
private final AuthConfig authConfig;
+ private static final String CHANGE_NUMBER_REGEX = "(?:/c)?/([1-9][0-9]*)";
+ private static final String PATCH_SET_REGEX = "([1-9][0-9]*(\\.\\.[1-9][0-9]*)?)";
+
UrlModule(AuthConfig authConfig) {
this.authConfig = authConfig;
}
@@ -72,7 +75,12 @@
serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS));
serveRegex("^/register$").with(registerScreen(false));
serveRegex("^/register/(.+)$").with(registerScreen(true));
- serveRegex("^(?:/c)?/([1-9][0-9]*)/?.*$").with(NumericChangeIdRedirectServlet.class);
+ serveRegex("^" + CHANGE_NUMBER_REGEX + "(/" + PATCH_SET_REGEX + ")?/?$")
+ .with(NumericChangeIdRedirectServlet.class);
+ serveRegex("^" + CHANGE_NUMBER_REGEX + "/" + PATCH_SET_REGEX + "?/[^+]+$")
+ .with(NumericChangeIdRedirectServlet.class);
+ serveRegex("^" + CHANGE_NUMBER_REGEX + "/comment/\\w+/?$")
+ .with(NumericChangeIdRedirectServlet.class);
serveRegex("^/p/(.*)$").with(queryProjectNew());
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
diff --git a/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java b/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
index 1e043b1..1316066 100644
--- a/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
+++ b/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
@@ -22,6 +22,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.io.PrintWriter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -52,8 +53,9 @@
res.setContentType("image/svg+xml");
res.setCharacterEncoding(UTF_8.name());
res.setStatus(HttpServletResponse.SC_OK);
- res.getWriter().write(responseToClient);
- res.getWriter().flush();
+ PrintWriter writer = res.getWriter();
+ writer.write(responseToClient);
+ writer.flush();
} else {
res.setContentLength(0);
res.setStatus(HttpServletResponse.SC_NO_CONTENT);
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index b00294f..42e10d8 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -63,7 +63,17 @@
public class StaticModule extends ServletModule {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static final String CHANGE_NUMBER_URI_REGEX = "^(?:/c)?/([1-9][0-9]*)/?.*";
+ // This constant is copied and NOT reused from UrlModule because of the need for
+ // StaticModule and UrlModule to be used in isolation. The requirement comes
+ // from the way Google includes these two classes in their setup.
+ private static final String CHANGE_NUMBER_REGEX = "(?:/c)?/([1-9][0-9]*)";
+ // Regex matching the direct links to comments using only the change number
+ // 1234/comment/abc_def
+ public static final String CHANGE_NUMBER_URI_REGEX =
+ "^"
+ + CHANGE_NUMBER_REGEX
+ + "(/[1-9][0-9]*(\\.\\.[1-9][0-9]*)?(/[^+]*)?)?(/comment/[^+]+)?/?$";
+
private static final Pattern CHANGE_NUMBER_URI_PATTERN = Pattern.compile(CHANGE_NUMBER_URI_REGEX);
/**
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index e800d17..d393a89 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -104,6 +104,7 @@
private Injector sysInjector;
private Injector cfgInjector;
private Config globalConfig;
+ private boolean reuseExistingDocuments;
@Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
@Inject private DynamicMap<Cache<?, ?>> cacheMap;
@@ -120,6 +121,10 @@
cfgInjector = dbInjector.createChildInjector();
globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
overrideConfig();
+ reuseExistingDocuments =
+ reuseExistingDocumentsOption != null
+ ? reuseExistingDocumentsOption
+ : globalConfig.getBoolean("index", null, "reuseExistingDocuments", false);
LifecycleManager dbManager = new LifecycleManager();
dbManager.add(dbInjector);
dbManager.start();
@@ -221,7 +226,8 @@
super.configure();
OptionalBinder.newOptionalBinder(binder(), IsFirstInsertForEntry.class)
.setBinding()
- .toInstance(IsFirstInsertForEntry.YES);
+ .toInstance(
+ reuseExistingDocuments ? IsFirstInsertForEntry.NO : IsFirstInsertForEntry.YES);
OptionalBinder.newOptionalBinder(binder(), BuildBloomFilter.class)
.setBinding()
.toInstance(buildBloomFilter ? BuildBloomFilter.TRUE : BuildBloomFilter.FALSE);
@@ -265,10 +271,6 @@
requireNonNull(
index, () -> String.format("no active search index configured for %s", def.getName()));
index.markReady(false);
- boolean reuseExistingDocuments =
- reuseExistingDocumentsOption != null
- ? reuseExistingDocumentsOption
- : globalConfig.getBoolean("index", null, "reuseExistingDocuments", false);
if (!reuseExistingDocuments) {
index.deleteAll();
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 914bdd2..43270df 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -23,11 +23,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache.AllExternalGroupsProto;
@@ -44,9 +46,11 @@
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -61,6 +65,8 @@
private static final String EXTERNAL_NAME = "groups_external";
private static final String PERSISTED_EXTERNAL_NAME = "groups_external_persisted";
+ private final IndexConfig indexConfig;
+
public static Module module() {
return new CacheModule() {
@Override
@@ -114,10 +120,12 @@
LoadingCache<Account.Id, ImmutableSet<AccountGroup.UUID>> groupsWithMember,
@Named(PARENT_GROUPS_NAME)
LoadingCache<AccountGroup.UUID, ImmutableSet<AccountGroup.UUID>> parentGroups,
- @Named(EXTERNAL_NAME) LoadingCache<String, ImmutableList<AccountGroup.UUID>> external) {
+ @Named(EXTERNAL_NAME) LoadingCache<String, ImmutableList<AccountGroup.UUID>> external,
+ IndexConfig indexConfig) {
this.groupsWithMember = groupsWithMember;
this.parentGroups = parentGroups;
this.external = external;
+ this.indexConfig = indexConfig;
}
@Override
@@ -144,7 +152,10 @@
public Collection<AccountGroup.UUID> parentGroupsOf(Set<AccountGroup.UUID> groupIds) {
try {
Set<AccountGroup.UUID> parents = new HashSet<>();
- parentGroups.getAll(groupIds).values().forEach(p -> parents.addAll(p));
+ for (List<AccountGroup.UUID> groupIdsBatch :
+ Lists.partition(new ArrayList<>(groupIds), indexConfig.maxTerms())) {
+ parentGroups.getAll(groupIdsBatch).values().forEach(p -> parents.addAll(p));
+ }
return parents;
} catch (ExecutionException e) {
logger.atWarning().withCause(e).log("Cannot load included groups");
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 2331255..fb02de6 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -222,6 +222,9 @@
isIndexed.set(i);
newChildren.add(c);
} else if (nc == null /* cannot rewrite c */) {
+ if (c instanceof ChangeDataSource) {
+ changeSource.set(i);
+ }
notIndexed.set(i);
newChildren.add(c);
} else {
@@ -236,6 +239,9 @@
if (isIndexed.cardinality() == n) {
return in; // All children are indexed, leave as-is for parent.
} else if (notIndexed.cardinality() == n) {
+ if (changeSource.cardinality() == n) {
+ return copy(in, newChildren);
+ }
return null; // Can't rewrite any children, so cannot rewrite in.
} else if (rewritten.cardinality() == n) {
// All children were rewritten.
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/patch/ApplyPatchUtil.java
similarity index 99%
rename from java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
rename to java/com/google/gerrit/server/patch/ApplyPatchUtil.java
index 8b8aaa6..a319a11 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/patch/ApplyPatchUtil.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.restapi.change;
+package com.google.gerrit.server.patch;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
index 8d5247d..6b7f563 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.ApplyPatchUtil;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ContributorAgreementsChecker;
import com.google.gerrit.server.project.InvalidChangeOperationException;
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 746313b..4c3c7b0 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -74,6 +74,7 @@
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.MergeUtilFactory;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.ApplyPatchUtil;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index eff783e..1d2d048 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -56,7 +56,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.restapi.change.ApplyPatchUtil;
+import com.google.gerrit.server.patch.ApplyPatchUtil;
import com.google.inject.Inject;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
diff --git a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
index 8011fb0..a54c7ca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
@@ -454,12 +454,21 @@
ChangeData changeData = createChange().getChange();
int changeNumber = changeData.getId().get();
- String finalSegment = "any/Thing";
+ assertChangeNumberWithSuffixRedirected(changeNumber, "1..2");
+ assertChangeNumberWithSuffixRedirected(changeNumber, "2");
+ assertChangeNumberWithSuffixRedirected(changeNumber, "2/COMMIT_MSG");
+ assertChangeNumberWithSuffixRedirected(changeNumber, "2?foo=bar");
+ assertChangeNumberWithSuffixRedirected(changeNumber, "2/path/to/source/file/MyClass.java");
+ }
- String redirectUri = String.format("/c/%s/+/%d/%s", project.get(), changeNumber, finalSegment);
+ private void assertChangeNumberWithSuffixRedirected(int changeNumber, String suffix)
+ throws Exception {
+ String redirectUri =
+ anonymousRestSession.getUrl(
+ String.format("/c/%s/+/%d/%s", project.get(), changeNumber, suffix));
anonymousRestSession
- .get(String.format("/c/%d/%s", changeNumber, finalSegment))
- .assertTemporaryRedirect(redirectUri);
+ .get(String.format("/c/%d/%s", changeNumber, suffix))
+ .assertTemporaryRedirectUri(redirectUri);
}
@Test
@@ -467,12 +476,12 @@
int changeNumber = createChange().getChange().getId().get();
String commentId = "ff3303fd_8341647b";
- String redirectUri =
+ String redirectPath =
String.format("/c/%s/+/%d/comment/%s", project.get(), changeNumber, commentId);
anonymousRestSession
.get(String.format("/%s/comment/%s", changeNumber, commentId))
- .assertTemporaryRedirect(redirectUri);
+ .assertTemporaryRedirect(redirectPath);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 20ed40d..04093a5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -83,7 +83,7 @@
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.restapi.change.ApplyPatchUtil;
+import com.google.gerrit.server.patch.ApplyPatchUtil;
import com.google.gerrit.server.restapi.change.CreateChange;
import com.google.gerrit.server.restapi.change.CreateChange.CommitTreeSupplier;
import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
diff --git a/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java b/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java
index 28ec30d..e973a26 100644
--- a/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java
@@ -15,19 +15,33 @@
package com.google.gerrit.httpd.raw;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.raw.StaticModule.PolyGerritFilter.isPolyGerritIndex;
-import com.google.common.collect.ImmutableList;
import org.junit.Test;
public class StaticModuleTest {
@Test
public void doNotMatchPolyGerritIndex() {
- ImmutableList.of(
- "/c/123456/anyString",
- "/123456/anyString",
- "/c/123456/comment/9ab75172_67d798e1",
- "/123456/comment/9ab75172_67d798e1")
- .forEach(url -> assertThat(StaticModule.PolyGerritFilter.isPolyGerritIndex(url)).isFalse());
+ assertThat(isPolyGerritIndex("/123456")).isFalse();
+ assertThat(isPolyGerritIndex("/123456/")).isFalse();
+ assertThat(isPolyGerritIndex("/123456/1")).isFalse();
+ assertThat(isPolyGerritIndex("/123456/1/")).isFalse();
+ assertThat(isPolyGerritIndex("/c/123456/comment/9ab75172_67d798e1")).isFalse();
+ assertThat(isPolyGerritIndex("/123456/comment/9ab75172_67d798e1")).isFalse();
+ assertThat(isPolyGerritIndex("/123456/comment/9ab75172_67d798e1/")).isFalse();
+ assertThat(isPolyGerritIndex("/123456/1..2")).isFalse();
+ assertThat(isPolyGerritIndex("/c/123456/1..2")).isFalse();
+ assertThat(isPolyGerritIndex("/c/2/1/COMMIT_MSG")).isFalse();
+ assertThat(isPolyGerritIndex("/c/2/1/path/to/source/file/MyClass.java")).isFalse();
+ }
+
+ @Test
+ public void matchPolyGerritIndex() {
+ assertThat(isPolyGerritIndex("/c/test/+/123456/anyString")).isTrue();
+ assertThat(isPolyGerritIndex("/c/test/+/123456/comment/9ab75172_67d798e1")).isTrue();
+ assertThat(isPolyGerritIndex("/c/321/+/123456/anyString")).isTrue();
+ assertThat(isPolyGerritIndex("/c/321/+/123456/comment/9ab75172_67d798e1")).isTrue();
+ assertThat(isPolyGerritIndex("/c/321/anyString")).isTrue();
}
}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 26e9e54..c65e552 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -38,6 +38,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.OrSource;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Set;
@@ -46,6 +47,7 @@
public class ChangeIndexRewriterTest {
private static final IndexConfig CONFIG = IndexConfig.createDefault();
+ private static final int MAX_INDEX_QUERY_TERMS = 4;
private FakeChangeIndex index;
private ChangeIndexCollection indexes;
@@ -58,7 +60,9 @@
indexes = new ChangeIndexCollection();
indexes.setSearchIndex(index);
queryBuilder = new FakeQueryBuilder(indexes);
- rewrite = new ChangeIndexRewriter(indexes, IndexConfig.builder().maxTerms(3).build());
+ rewrite =
+ new ChangeIndexRewriter(
+ indexes, IndexConfig.builder().maxTerms(MAX_INDEX_QUERY_TERMS).build());
}
@Test
@@ -71,7 +75,7 @@
public void nonIndexPredicate() throws Exception {
Predicate<ChangeData> in = parse("foo:a");
Predicate<ChangeData> out = rewrite(in);
- assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
+ assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
assertThat(out.getChildren())
.containsExactly(
query(
@@ -90,10 +94,24 @@
}
@Test
+ public void indexedOrSourceSubexpressions() throws Exception {
+ Predicate<ChangeData> in = parse("(file:a bar:b) OR (file:c bar:d)");
+ Predicate<ChangeData> out = rewrite(in);
+ assertThat(out.getClass()).isSameInstanceAs(OrSource.class);
+ assertThat(out.getChildCount()).isEqualTo(2);
+ assertThat(out.getChild(0).getChildren())
+ .containsExactly(query(parse("file:a")), parse("bar:b"))
+ .inOrder();
+ assertThat(out.getChild(1).getChildren())
+ .containsExactly(query(parse("file:c")), parse("bar:d"))
+ .inOrder();
+ }
+
+ @Test
public void nonIndexPredicates() throws Exception {
Predicate<ChangeData> in = parse("foo:a OR foo:b");
Predicate<ChangeData> out = rewrite(in);
- assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
+ assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
assertThat(out.getChildren())
.containsExactly(
query(
@@ -106,10 +124,26 @@
}
@Test
+ public void nonIndexOrSourcePredicates() throws Exception {
+ Predicate<ChangeData> in = parse("baz:a OR baz:b");
+ Predicate<ChangeData> out = rewrite(in);
+ assertThat(out.getClass()).isSameInstanceAs(OrSource.class);
+ assertThat(out.getChildren()).containsExactly(parse("baz:a"), parse("baz:b")).inOrder();
+ }
+
+ @Test
+ public void nonIndexAndSourcePredicates() throws Exception {
+ Predicate<ChangeData> in = parse("baz:a baz:b");
+ Predicate<ChangeData> out = rewrite(in);
+ assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
+ assertThat(out.getChildren()).containsExactly(parse("baz:a"), parse("baz:b")).inOrder();
+ }
+
+ @Test
public void oneIndexPredicate() throws Exception {
Predicate<ChangeData> in = parse("foo:a file:b");
Predicate<ChangeData> out = rewrite(in);
- assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
+ assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
assertThat(out.getChildren()).containsExactly(query(parse("file:b")), parse("foo:a")).inOrder();
}
@@ -167,7 +201,7 @@
public void indexAndNonIndexPredicates() throws Exception {
Predicate<ChangeData> in = parse("status:new bar:p file:a");
Predicate<ChangeData> out = rewrite(in);
- assertThat(AndChangeSource.class).isSameInstanceAs(out.getClass());
+ assertThat(out.getClass()).isSameInstanceAs(AndChangeSource.class);
assertThat(out.getChildren())
.containsExactly(query(andCardinal(parse("status:new"), parse("file:a"))), parse("bar:p"))
.inOrder();
@@ -239,12 +273,12 @@
@Test
public void tooManyTerms() throws Exception {
- String q = "file:a OR file:b OR file:c";
+ String q = "file:a OR file:b OR file:c OR file:d";
Predicate<ChangeData> in = parse(q);
assertEquals(query(in), rewrite(in));
QueryParseException thrown =
- assertThrows(QueryParseException.class, () -> rewrite(parse(q + " OR file:d")));
+ assertThrows(QueryParseException.class, () -> rewrite(parse(q + " OR file:e")));
assertThat(thrown).hasMessageThat().contains("too many terms in query");
}
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 90a9b9d..d816719 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -14,16 +14,48 @@
package com.google.gerrit.server.index.change;
+import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.OperatorPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.ResultSet;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import org.eclipse.jgit.lib.Config;
import org.junit.Ignore;
@Ignore
public class FakeQueryBuilder extends ChangeQueryBuilder {
+ public static class FakeNonIndexSourcePredicate extends OperatorPredicate<ChangeData>
+ implements ChangeDataSource {
+ private static final String operator = "baz";
+
+ public FakeNonIndexSourcePredicate(String value) {
+ super(operator, value);
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() {
+ return null;
+ }
+
+ @Override
+ public ResultSet<FieldBundle> readRaw() {
+ return null;
+ }
+
+ @Override
+ public int getCardinality() {
+ return 0;
+ }
+
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+ }
+
FakeQueryBuilder(ChangeIndexCollection indexes) {
super(
new QueryBuilder.Definition<>(FakeQueryBuilder.class),
@@ -64,6 +96,11 @@
}
@Operator
+ public Predicate<ChangeData> baz(String value) {
+ return new FakeNonIndexSourcePredicate(value);
+ }
+
+ @Operator
public Predicate<ChangeData> foo(String value) {
return predicate("foo", value);
}
diff --git a/modules/jgit b/modules/jgit
index 692ccfc..1cd87ab 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 692ccfc0c29d53afc7a0b82f41efcd999ed217b0
+Subproject commit 1cd87ab79065b78a0774f20f1bfd522747c37c15
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 783adf7..e5e9ece 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 783adf7ddf19924d054a1596eec6dd3da9f4aafe
+Subproject commit e5e9ece112242397f000660c6cee8f5053ca5da5
diff --git a/plugins/package.json b/plugins/package.json
index 78f990d..3da1b7a 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -19,6 +19,7 @@
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.7.0",
+ "@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.2",
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 3391423..c6d2b18 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -222,7 +222,7 @@
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
-"@codemirror/lang-vue@^0.1.1":
+"@codemirror/lang-vue@^0.1.1", "@codemirror/lang-vue@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz#bf79b9152cc18b4903d64c1f67e186ae045c8a97"
integrity sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 044693e..382a043 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -671,6 +671,16 @@
export type EmailAddress = BrandType<string, '_emailAddress'>;
/**
+ * The EmailInfo entity contains information about an email address of a user
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#email-info
+ */
+export declare interface EmailInfo {
+ email: EmailAddress;
+ preferred?: boolean;
+ pending_confirmation?: boolean;
+}
+
+/**
* The FetchInfo entity contains information about how to fetch a patchset via
* a certain protocol.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fetch-info
@@ -1084,6 +1094,7 @@
user: UserConfigInfo;
default_theme?: string;
submit_requirement_dashboard_columns?: string[];
+ metadata?: MetadataInfo[];
}
/**
@@ -1094,6 +1105,17 @@
*/
export type SshdInfo = {};
+/**
+ * The MetadataInfo entity contains contains metadata provided by plugins.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#metadata-info
+ */
+export declare interface MetadataInfo {
+ name: string;
+ value?: string;
+ description?: string;
+ web_links?: WebLinkInfo[];
+}
+
// Timestamps are given in UTC and have the format
// "'yyyy-mm-dd hh:mm:ss.fffffffff'"
// where "'ffffffffff'" represents nanoseconds.
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 21e032b..58827fb 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -17,6 +17,7 @@
import '../gr-repo-dashboards/gr-repo-dashboards';
import '../gr-repo-detail-list/gr-repo-detail-list';
import '../gr-repo-list/gr-repo-list';
+import '../gr-server-info/gr-server-info';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
AccountDetailInfo,
@@ -213,6 +214,7 @@
${this.renderGroupMembers()} ${this.renderGroupAuditLog()}
${this.renderRepoDetailList()} ${this.renderRepoCommands()}
${this.renderRepoAccess()} ${this.renderRepoDashboards()}
+ ${this.renderServerInfo()}
`;
}
@@ -447,6 +449,18 @@
`;
}
+ private renderServerInfo() {
+ if (this.view !== GerritView.ADMIN) return nothing;
+ if (this.adminViewState?.adminView !== AdminChildView.SERVER_INFO)
+ return nothing;
+
+ return html`
+ <div class="main table">
+ <gr-server-info class="table"></gr-server-info>
+ </div>
+ `;
+ }
+
override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('groupId')) {
this.computeGroupName();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index d184f35..cbac9de 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -84,7 +84,7 @@
Promise.resolve(createAdminCapabilities())
);
await element.reload();
- assert.equal(element.filteredLinks!.length, 3);
+ assert.equal(element.filteredLinks!.length, 4);
// Repos
assert.isNotOk(element.filteredLinks![0].subsection);
@@ -98,7 +98,7 @@
test('filteredLinks non admin authenticated', async () => {
await element.reload();
- assert.equal(element.filteredLinks!.length, 2);
+ assert.equal(element.filteredLinks!.length, 3);
// Repos
assert.isNotOk(element.filteredLinks![0].subsection);
// Groups
@@ -162,7 +162,7 @@
);
await element.reload();
await element.updateComplete;
- assert.equal(queryAll<HTMLLIElement>(element, '.sectionTitle').length, 3);
+ assert.equal(queryAll<HTMLLIElement>(element, '.sectionTitle').length, 4);
assert.equal(
queryAndAssert<HTMLSpanElement>(element, '.breadcrumbText').innerText,
'Test Repo'
@@ -189,7 +189,7 @@
);
await element.reload();
await element.updateComplete;
- assert.equal(element.filteredLinks!.length, 3);
+ assert.equal(element.filteredLinks!.length, 4);
// Repos
assert.isNotOk(element.filteredLinks![0].subsection);
// Groups
@@ -385,6 +385,12 @@
url: '/admin/plugins',
view: 'gr-plugin-list' as GerritView,
},
+ {
+ name: 'Server Info',
+ section: 'Server Info',
+ url: '/admin/server-info',
+ view: 'gr-server-info' as GerritView,
+ },
];
const expectedSubsectionLinks = [
{
@@ -532,6 +538,11 @@
Plugins
</a>
</li>
+ <li class="sectionTitle">
+ <a class="title" href="/admin/server-info" rel="noopener">
+ Server Info
+ </a>
+ </li>
</ul>
</gr-page-nav>
<div class="main table">
diff --git a/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts b/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts
new file mode 100644
index 0000000..b67239e
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts
@@ -0,0 +1,172 @@
+/**
+ * @license
+ * Copyright (C) 2024 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 '../../shared/gr-weblink/gr-weblink';
+import {MetadataInfo, ServerInfo, WebLinkInfo} from '../../../types/common';
+import {configModelToken} from '../../../models/config/config-model';
+import {customElement, state} from 'lit/decorators.js';
+import {css, html, LitElement} from 'lit';
+import {fireTitleChange} from '../../../utils/event-util';
+import {map} from 'lit/directives/map.js';
+import {resolve} from '../../../models/dependency';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subscribe} from '../../lit/subscription-controller';
+import {tableStyles} from '../../../styles/gr-table-styles';
+
+@customElement('gr-server-info')
+export class GrServerInfo extends LitElement {
+ @state() serverInfo?: ServerInfo;
+
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getConfigModel().serverConfig$,
+ serverInfo => {
+ this.serverInfo = serverInfo;
+ }
+ );
+ }
+
+ static override get styles() {
+ return [
+ tableStyles,
+ sharedStyles,
+ css`
+ .genericList tr td:last-of-type {
+ text-align: left;
+ }
+ .genericList tr th:last-of-type {
+ text-align: left;
+ }
+ .metadataDescription,
+ .metadataName,
+ .metadataValue,
+ .metadataWebLinks {
+ white-space: nowrap;
+ }
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ }
+ `,
+ ];
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ fireTitleChange('Server Info');
+ }
+
+ override render() {
+ return html`
+ <main class="gr-form-styles read-only">
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="metadataName topHeader">Name</th>
+ <th class="metadataValue topHeader">Value</th>
+ <th class="metadataWebLinks topHeader">Links</th>
+ <th class="metadataDescription topHeader">Description</th>
+ </tr>
+ </tbody>
+ ${this.renderServerInfoTable()}
+ </table>
+ </main>
+ `;
+ }
+
+ private renderServerInfoTable() {
+ return html`
+ <tbody>
+ ${map(this.getServerInfoAsMetadataInfos(), metadata =>
+ this.renderServerInfo(metadata)
+ )}
+ </tbody>
+ `;
+ }
+
+ private renderServerInfo(metadata: MetadataInfo) {
+ return html`
+ <tr class="table">
+ <td class="metadataName">${metadata.name}</td>
+ <td class="metadataValue">
+ ${metadata.value
+ ? metadata.value
+ : html`<span class="placeholder">--</span>`}
+ </td>
+ <td class="metadataWebLinks">
+ ${metadata.web_links
+ ? map(metadata.web_links, webLink => this.renderWebLink(webLink))
+ : ''}
+ </td>
+ <td class="metadataDescription">
+ ${metadata.description ? metadata.description : ''}
+ </td>
+ </tr>
+ `;
+ }
+
+ private renderWebLink(info: WebLinkInfo) {
+ return html`<p><gr-weblink imageAndText .info=${info}></gr-weblink></p>`;
+ }
+
+ private getServerInfoAsMetadataInfos() {
+ let metadataList = new Array<MetadataInfo>();
+
+ const accountsVisibilityMetadata = this.createAccountVisibilityMetadata();
+ if (accountsVisibilityMetadata) {
+ metadataList.push(accountsVisibilityMetadata);
+ }
+
+ if (this.serverInfo?.metadata) {
+ metadataList = metadataList.concat(this.serverInfo.metadata);
+ }
+
+ return metadataList;
+ }
+
+ private createAccountVisibilityMetadata(): MetadataInfo | undefined {
+ if (this.serverInfo?.accounts?.visibility) {
+ const accountsVisibilityMetadata = {
+ name: 'accounts.visibility',
+ value: this.serverInfo.accounts.visibility,
+ description:
+ "Controls visibility of other users' dashboard pages and completion suggestions to web users.",
+ web_links: new Array<WebLinkInfo>(),
+ };
+ if (this.serverInfo?.gerrit?.doc_url) {
+ const docWebLink = {
+ name: 'Documentation',
+ url:
+ this.serverInfo.gerrit.doc_url +
+ 'config-gerrit.html#accounts.visibility',
+ };
+ accountsVisibilityMetadata.web_links.push(docWebLink);
+ }
+ return accountsVisibilityMetadata;
+ }
+ return undefined;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-server-info': GrServerInfo;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index 91770ca..0799e03 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -172,7 +172,7 @@
}
override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('changeSection')) {
+ if (changedProperties.has('changeSection') && this.isLoggedIn) {
// In case the list of changes is updated due to auto reloading, we want
// to ensure the model removes any stale change that is not a part of the
// new section changes.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
index 4655c71..ae55a4d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
@@ -19,6 +19,7 @@
ChangeInfoId,
ChangeStatus,
CommitId,
+ EmailAddress,
GitRef,
HttpMethod,
NumericChangeId,
@@ -67,11 +68,11 @@
const emails = [
{
- email: 'primary@email.com',
+ email: 'primary@email.com' as EmailAddress,
preferred: true,
},
{
- email: 'secondary@email.com',
+ email: 'secondary@email.com' as EmailAddress,
preferred: false,
},
];
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index 038fcd5..1c2a89d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -174,7 +174,7 @@
test('hide rebaseWithCommitterEmail dialog when committer has single email', async () => {
element.committerEmailDropdownItems = [
{
- email: 'test1@example.com',
+ email: 'test1@example.com' as EmailAddress,
preferred: true,
pending_confirmation: true,
},
@@ -186,12 +186,12 @@
test('show rebaseWithCommitterEmail dialog when committer has more than one email', async () => {
element.committerEmailDropdownItems = [
{
- email: 'test1@example.com',
+ email: 'test1@example.com' as EmailAddress,
preferred: true,
pending_confirmation: true,
},
{
- email: 'test2@example.com',
+ email: 'test2@example.com' as EmailAddress,
pending_confirmation: true,
},
];
@@ -230,12 +230,12 @@
};
element.committerEmailDropdownItems = [
{
- email: 'currentuser1@example.com',
+ email: 'currentuser1@example.com' as EmailAddress,
preferred: true,
pending_confirmation: true,
},
{
- email: 'currentuser2@example.com',
+ email: 'currentuser2@example.com' as EmailAddress,
pending_confirmation: true,
},
];
@@ -264,12 +264,12 @@
};
element.committerEmailDropdownItems = [
{
- email: 'uploader1@example.com',
+ email: 'uploader1@example.com' as EmailAddress,
preferred: true,
pending_confirmation: true,
},
{
- email: 'uploader2@example.com',
+ email: 'uploader2@example.com' as EmailAddress,
preferred: false,
pending_confirmation: true,
},
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 30f4bf8..28ca5fd 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -46,6 +46,7 @@
AdminViewModel,
AdminViewState,
PLUGIN_LIST_ROUTE,
+ SERVER_INFO_ROUTE,
} from '../../../models/views/admin';
import {
AgreementViewModel,
@@ -178,6 +179,9 @@
// Matches /admin/repos/$REPO,tags with optional filter and offset.
TAG_LIST: /^\/admin\/repos\/(.+),tags\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+ // Matches /admin/server-info.
+ SERVER_INFO: /^\/admin\/server-info$/,
+
QUERY: /^\/q\/(.+?)(,(\d+))?$/,
/**
@@ -930,6 +934,13 @@
this.handlePluginScreen(ctx)
);
+ this.mapRouteState(
+ SERVER_INFO_ROUTE,
+ this.adminViewModel,
+ 'handleServerInfoRoute',
+ true
+ );
+
this.mapRoute(
RoutePattern.DOCUMENTATION_SEARCH_FILTER,
'handleDocumentationSearchRoute',
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 6f4e528..4a78140 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -161,6 +161,7 @@
'handlePluginListRoute',
'handleRepoCommandsRoute',
'handleRepoEditFileRoute',
+ 'handleServerInfoRoute',
'handleSettingsLegacyRoute',
'handleSettingsRoute',
];
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 4814733..29d2bf0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -127,6 +127,7 @@
'line-selected': CustomEvent<LineSelectedEventDetail>;
// Fired if being logged in is required.
'show-auth-required': CustomEvent<{}>;
+ 'reload-diff': CustomEvent<{path: string | undefined}>;
}
}
@@ -343,6 +344,11 @@
this.addEventListener('diff-context-expanded', event =>
this.handleDiffContextExpanded(event)
);
+ this.addEventListener('reload-diff', (e: CustomEvent) => {
+ if (e.detail.path === this.path) {
+ this.reload(false);
+ }
+ });
subscribe(
this,
() => this.getBrowserModel().diffViewMode$,
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
index 84ed8dc..12321c2 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
@@ -8,6 +8,7 @@
import {GrEmailEditor} from './gr-email-editor';
import {spyRestApi, stubRestApi} from '../../../test/test-utils';
import {fixture, html, assert} from '@open-wc/testing';
+import {EmailAddress} from '../../../api/rest-api';
suite('gr-email-editor tests', () => {
let element: GrEmailEditor;
@@ -15,9 +16,9 @@
setup(async () => {
const emails = [
- {email: 'email@one.com'},
- {email: 'email@two.com', preferred: true},
- {email: 'email@three.com'},
+ {email: 'email@one.com' as EmailAddress},
+ {email: 'email@two.com' as EmailAddress, preferred: true},
+ {email: 'email@three.com' as EmailAddress},
];
accountEmailStub = stubRestApi('getAccountEmails').returns(
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index d238881..b55c9fd 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -91,7 +91,7 @@
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {deepEqual} from '../../../utils/deep-util';
import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
-import {waitUntil} from '../../../utils/async-util';
+import {noAwait, waitUntil} from '../../../utils/async-util';
import {
AutocompleteCache,
AutocompletionContext,
@@ -254,6 +254,10 @@
this.comment?.fix_suggestions?.[0];
@state()
+ previewedGeneratedFixSuggestion: FixSuggestionInfo | undefined =
+ this.comment?.fix_suggestions?.[0];
+
+ @state()
generatedSuggestionId?: string;
@state()
@@ -1184,6 +1188,18 @@
}
}
+ // visible for testing
+ async waitPreviewForGeneratedSuggestion() {
+ const generatedFixSuggestion = this.generatedFixSuggestion;
+ if (!generatedFixSuggestion) return;
+ await waitUntil(
+ () =>
+ !!this.suggestionDiffPreview?.previewed &&
+ this.suggestionDiffPreview?.previewLoadedFor === generatedFixSuggestion
+ );
+ this.previewedGeneratedFixSuggestion = generatedFixSuggestion;
+ }
+
private renderGenerateSuggestEditButton() {
if (!this.showGeneratedSuggestion()) {
return nothing;
@@ -1322,6 +1338,7 @@
return;
}
this.generatedFixSuggestion = suggestion;
+ noAwait(this.waitPreviewForGeneratedSuggestion());
try {
await waitUntil(() => this.getFixSuggestions() !== undefined);
@@ -1558,6 +1575,9 @@
assert(isDraft(this.comment), 'only drafts are editable');
if (this.editing) return;
this.editing = true;
+ // For quickly opening and closing the comment, the suggestion diff preview
+ // might not have time to load and preview.
+ noAwait(this.waitPreviewForGeneratedSuggestion());
}
// TODO: Move this out of gr-comment. gr-comment should not have a comments
@@ -1841,13 +1861,9 @@
// Disable fix suggestions when the comment already has a user suggestion
if (this.comment && hasUserSuggestion(this.comment)) return undefined;
// we ignore fixSuggestions until they are previewed.
- if (
- this.suggestionDiffPreview &&
- !this.suggestionDiffPreview?.previewed &&
- !this.suggestionLoading
- )
- return undefined;
- return [this.generatedFixSuggestion];
+ if (this.previewedGeneratedFixSuggestion)
+ return [this.previewedGeneratedFixSuggestion];
+ return undefined;
}
private handleToggleResolved() {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 5273439..7292c64 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -1158,6 +1158,9 @@
'#suggestionDiffPreview'
);
suggestionDiffPreview.previewed = true;
+ suggestionDiffPreview.previewLoadedFor = generatedFixSuggestion;
+ await element.updateComplete;
+ await element.waitPreviewForGeneratedSuggestion();
await element.updateComplete;
element.save();
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index dbb89db..fe96d56 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -15,6 +15,7 @@
import {GrDropdownList} from '../gr-dropdown-list/gr-dropdown-list';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
+ EmailAddress,
NumericChangeId,
RepoName,
RevisionPatchSetNum,
@@ -23,11 +24,11 @@
const emails = [
{
- email: 'primary@example.com',
+ email: 'primary@example.com' as EmailAddress,
preferred: true,
},
{
- email: 'secondary@example.com',
+ email: 'secondary@example.com' as EmailAddress,
preferred: false,
},
];
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
index ca7fe9c..36266a0 100644
--- a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -7,7 +7,7 @@
import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
-import {css, html, LitElement} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, state, query, property} from 'lit/decorators.js';
import {fire} from '../../../utils/event-util';
import {getDocUrl} from '../../../utils/url-util';
@@ -16,7 +16,7 @@
import {configModelToken} from '../../../models/config/config-model';
import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
import {changeModelToken} from '../../../models/change/change-model';
-import {Comment, PatchSetNumber} from '../../../types/common';
+import {Comment, NumericChangeId, PatchSetNumber} from '../../../types/common';
import {OpenFixPreviewEventDetail} from '../../../types/events';
import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
import {SuggestionsProvider} from '../../../api/suggestions';
@@ -25,6 +25,7 @@
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {getAppContext} from '../../../services/app-context';
import {Interaction} from '../../../constants/reporting';
+import {isFileUnchanged} from '../../../utils/diff-util';
export const COLLAPSE_SUGGESTION_STORAGE_KEY = 'collapseSuggestionStorageKey';
@@ -47,11 +48,15 @@
@state() latestPatchNum?: PatchSetNumber;
+ @state() changeNum?: NumericChangeId;
+
@state()
suggestionsProvider?: SuggestionsProvider;
@state() private isOwner = false;
+ @state() private enableApplyOnUnModifiedFile = false;
+
/**
* This is just a reflected property such that css rules can be based on it.
*/
@@ -68,6 +73,8 @@
private readonly reporting = getAppContext().reportingService;
+ private readonly restApiService = getAppContext().restApiService;
+
constructor() {
super();
subscribe(
@@ -85,6 +92,17 @@
() => this.getChangeModel().isOwner$,
x => (this.isOwner = x)
);
+ subscribe(
+ this,
+ () => this.getChangeModel().changeNum$,
+ x => (this.changeNum = x)
+ );
+ }
+
+ override updated(changed: PropertyValues) {
+ if (changed.has('changeNum') || changed.has('latestPatchNum')) {
+ this.checkIfcanEnableApplyOnUnModifiedFile();
+ }
}
override connectedCallback() {
@@ -277,7 +295,9 @@
if (!this.comment?.fix_suggestions) return;
this.applyingFix = true;
try {
- await this.suggestionDiffPreview?.applyFixSuggestion();
+ await this.suggestionDiffPreview?.applyFixSuggestion(
+ this.enableApplyOnUnModifiedFile
+ );
} finally {
this.applyingFix = false;
}
@@ -285,6 +305,7 @@
private isApplyEditDisabled() {
if (this.comment?.patch_set === undefined) return true;
+ if (this.enableApplyOnUnModifiedFile) return false;
return this.comment.patch_set !== this.latestPatchNum;
}
@@ -294,6 +315,29 @@
? 'You cannot apply this fix because it is from a previous patchset'
: '';
}
+
+ private async checkIfcanEnableApplyOnUnModifiedFile() {
+ // if enabled we don't need to enable
+ if (!this.isApplyEditDisabled()) return;
+
+ const basePatchNum = this.comment?.patch_set;
+ const path = this.comment?.path;
+
+ if (!basePatchNum || !this.latestPatchNum || !path || !this.changeNum) {
+ return;
+ }
+
+ const diff = await this.restApiService.getDiff(
+ this.changeNum,
+ basePatchNum,
+ this.latestPatchNum,
+ path
+ );
+
+ if (diff && isFileUnchanged(diff)) {
+ this.enableApplyOnUnModifiedFile = true;
+ }
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index d67b842..900e9ed 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -307,11 +307,14 @@
private convertCodeToSuggestions() {
const marks = this.renderRoot.querySelectorAll('mark');
- for (const userSuggestionMark of marks) {
+ marks.forEach((userSuggestionMark, index) => {
const userSuggestion = document.createElement('gr-user-suggestion-fix');
// Temporary workaround for bug - tabs replacement
if (this.content.includes('\t')) {
- userSuggestion.textContent = getUserSuggestionFromString(this.content);
+ userSuggestion.textContent = getUserSuggestionFromString(
+ this.content,
+ index
+ );
} else {
userSuggestion.textContent = userSuggestionMark.textContent ?? '';
}
@@ -319,7 +322,7 @@
userSuggestion,
userSuggestionMark
);
- }
+ });
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 2da1fb8..310c360 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -355,12 +355,17 @@
try {
resp = await this.fetchImpl(fetchReq);
} catch (err) {
+ // Wrap the error to get more information about the stack.
+ const newErr = new Error(
+ `Network error when trying to fetch. Cause: ${(err as Error).message}`
+ );
+ newErr.stack = (newErr.stack ?? '') + '\n' + ((err as Error).stack ?? '');
if (req.errFn) {
- await req.errFn.call(undefined, null, err as Error);
+ await req.errFn.call(undefined, null, newErr);
} else {
- fireNetworkError(err as Error);
+ fireNetworkError(newErr);
}
- throw err;
+ throw newErr;
}
if (req.reportServerError && !resp.ok) {
if (req.errFn) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
index 0f94a4b..0f7acae 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
@@ -206,7 +206,10 @@
const promise = helper.fetchJSON({url: '/dummy/url'});
await assertReadRequest();
const err = await assertFails(promise);
- assert.equal((err as Error).message, 'No response');
+ assert.equal(
+ (err as Error).message,
+ 'Network error when trying to fetch. Cause: No response'
+ );
await waitEventLoop();
assert.isTrue(networkErrorCalled);
assert.isFalse(serverErrorCalled);
@@ -221,7 +224,10 @@
});
await assertReadRequest();
const err = await assertFails(promise);
- assert.equal((err as Error).message, 'No response');
+ assert.equal(
+ (err as Error).message,
+ 'Network error when trying to fetch. Cause: No response'
+ );
await waitEventLoop();
assert.isTrue(errFn.called);
assert.isFalse(networkErrorCalled);
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
index 504ee63..d059c7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
@@ -7,7 +7,13 @@
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {getAppContext} from '../../../services/app-context';
-import {Comment, EDIT, BasePatchSetNum, RepoName} from '../../../types/common';
+import {
+ Comment,
+ EDIT,
+ BasePatchSetNum,
+ PatchSetNumber,
+ RepoName,
+} from '../../../types/common';
import {anyLineTooLong} from '../../../utils/diff-util';
import {
DiffLayer,
@@ -79,6 +85,8 @@
@state()
diffPrefs?: DiffPreferencesInfo;
+ @state() latestPatchNum?: PatchSetNumber;
+
@state()
renderPrefs: RenderPreferences = {
disable_context_control_buttons: true,
@@ -120,6 +128,11 @@
);
subscribe(
this,
+ () => this.getChangeModel().latestPatchNum$,
+ x => (this.latestPatchNum = x)
+ );
+ subscribe(
+ this,
() => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
@@ -285,6 +298,7 @@
});
if (currentPreviews.length > 0) {
this.preview = currentPreviews[0];
+ this.previewed = true;
this.previewLoadedFor = this.fixSuggestionInfo;
}
@@ -299,9 +313,9 @@
* Similar code flow is in gr-apply-fix-dialog.handleApplyFix
* Used in gr-fix-suggestions
*/
- public applyFixSuggestion() {
+ public applyFixSuggestion(onLatestPatchset = false) {
if (this.suggestion || !this.fixSuggestionInfo) return;
- this.applyFix(this.fixSuggestionInfo);
+ return this.applyFix(this.fixSuggestionInfo, onLatestPatchset);
}
/**
@@ -323,7 +337,10 @@
this.applyFix(fixSuggestions[0]);
}
- private async applyFix(fixSuggestion: FixSuggestionInfo) {
+ private async applyFix(
+ fixSuggestion: FixSuggestionInfo,
+ onLatestPatchset = false
+ ) {
const changeNum = this.changeNum;
const basePatchNum = this.comment?.patch_set as BasePatchSetNum;
if (!changeNum || !basePatchNum || !fixSuggestion) return;
@@ -331,7 +348,7 @@
this.reporting.time(Timing.APPLY_FIX_LOAD);
const res = await this.restApiService.applyFixSuggestion(
changeNum,
- basePatchNum,
+ onLatestPatchset ? this.latestPatchNum ?? basePatchNum : basePatchNum,
fixSuggestion.replacements
);
this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
@@ -352,6 +369,7 @@
forceReload: !this.hasEdit,
})
);
+ fire(this, 'reload-diff', {path: this.comment?.path});
fire(this, 'apply-user-suggestion', {});
}
}
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 51bfcd9..5eb9cac 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -54,6 +54,8 @@
return {name: 'code'};
case LinkIcon.FILE_PRESENT:
return {name: 'file_present'};
+ case LinkIcon.VIEW_TIMELINE:
+ return {name: 'view_timeline'};
default:
// We don't throw an assertion error here, because plugins don't have to
// be written in TypeScript, so we may encounter arbitrary strings for
@@ -145,7 +147,9 @@
if (replacements.length === 0) return undefined;
return {
- description: fix.description || `Fix provided by ${checkName}`,
+ description: [fix.description, `Fix provided by ${checkName}`]
+ .filter(Boolean)
+ .join(' - '),
fix_id: PROVIDED_FIX_ID,
replacements,
};
diff --git a/polygerrit-ui/app/models/views/admin.ts b/polygerrit-ui/app/models/views/admin.ts
index 02b5796..164858d 100644
--- a/polygerrit-ui/app/models/views/admin.ts
+++ b/polygerrit-ui/app/models/views/admin.ts
@@ -59,10 +59,22 @@
},
};
+export const SERVER_INFO_ROUTE: Route<AdminViewState> = {
+ urlPattern: /^\/admin\/server-info$/,
+ createState: () => {
+ const state: AdminViewState = {
+ view: GerritView.ADMIN,
+ adminView: AdminChildView.SERVER_INFO,
+ };
+ return state;
+ },
+};
+
export enum AdminChildView {
REPOS = 'gr-repo-list',
GROUPS = 'gr-admin-group-list',
PLUGINS = 'gr-plugin-list',
+ SERVER_INFO = 'gr-server-info',
}
const ADMIN_LINKS: NavLink[] = [
{
@@ -84,6 +96,12 @@
url: createAdminUrl({adminView: AdminChildView.PLUGINS}),
view: 'gr-plugin-list' as GerritView,
},
+ {
+ name: 'Server Info',
+ section: 'Server Info',
+ url: createAdminUrl({adminView: AdminChildView.SERVER_INFO}),
+ view: 'gr-server-info' as GerritView,
+ },
];
export interface AdminLink {
@@ -277,6 +295,8 @@
return `${getBaseUrl()}/admin/groups`;
case AdminChildView.PLUGINS:
return `${getBaseUrl()}/admin/plugins`;
+ case AdminChildView.SERVER_INFO:
+ return `${getBaseUrl()}/admin/server-info`;
}
}
diff --git a/polygerrit-ui/app/models/views/admin_test.ts b/polygerrit-ui/app/models/views/admin_test.ts
index 5d142bf..1cd1897 100644
--- a/polygerrit-ui/app/models/views/admin_test.ts
+++ b/polygerrit-ui/app/models/views/admin_test.ts
@@ -55,6 +55,11 @@
assert.isNotOk(res.links[2].subsection);
}
+ if (expected.serverInfoShown) {
+ assert.equal(res.links[3].name, 'Server Info');
+ assert.isNotOk(res.links[3].subsection);
+ }
+
if (expected.projectPageShown) {
assert.isOk(res.links[0].subsection);
assert.equal(res.links[0].subsection!.children!.length, 6);
@@ -116,6 +121,7 @@
groupListShown: false,
groupPageShown: false,
pluginListShown: false,
+ serverInfoShown: false,
};
});
@@ -162,7 +168,7 @@
setup(() => {
expected = {
- totalLength: 2,
+ totalLength: 3,
pluginListShown: false,
};
capabilityStub.returns(Promise.resolve({}));
@@ -203,9 +209,10 @@
setup(() => {
capabilityStub.returns(Promise.resolve({viewPlugins: true}));
expected = {
- totalLength: 3,
+ totalLength: 4,
groupListShown: true,
pluginListShown: true,
+ serverInfoShown: true,
};
});
@@ -312,7 +319,7 @@
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
- totalLength: 4,
+ totalLength: 5,
pluginGeneratedLinks: generatedLinks,
});
await testAdminLinks(account, options, expected);
@@ -339,7 +346,7 @@
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
- totalLength: 3,
+ totalLength: 4,
pluginGeneratedLinks: [generatedLinks[0]],
});
await testAdminLinks(account, options, expected);
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index 05927f3..09ba661 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -18,6 +18,7 @@
PLUGIN_SCREEN = 'plugin-screen',
REPO = 'repo',
SEARCH = 'search',
+ SERVER_INFO = 'server-info',
SETTINGS = 'settings',
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index e12e1a8..ae684bf 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -60,6 +60,7 @@
EDIT,
EditPatchSet,
EmailAddress,
+ EmailInfo,
FetchInfo,
FileInfo,
GerritInfo,
@@ -81,6 +82,7 @@
LabelTypeInfoValues,
LabelValueToDescriptionMap,
MaxObjectSizeLimitInfo,
+ MetadataInfo,
NumericChangeId,
ParentCommitInfo,
PARENT,
@@ -163,6 +165,7 @@
DownloadSchemeInfo,
EditPatchSet,
EmailAddress,
+ EmailInfo,
FileInfo,
FixId,
FixSuggestionInfo,
@@ -185,6 +188,7 @@
LabelTypeInfoValues,
LabelValueToDescriptionMap,
MaxObjectSizeLimitInfo,
+ MetadataInfo,
NumericChangeId,
ParentCommitInfo,
PatchRange,
@@ -381,17 +385,7 @@
capabilities?: AccountCapabilityInfo;
groups: GroupInfo[];
external_ids: AccountExternalIdInfo[];
- metadata: AccountMetadataInfo[];
-}
-
-/**
- * The `AccountMetadataInfo` entity contains account metadata.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-metadata-info
- */
-export interface AccountMetadataInfo {
- name: string;
- value?: string;
- description?: string;
+ metadata: MetadataInfo[];
}
/**
@@ -1020,16 +1014,6 @@
}
/**
- * The EmailInfo entity contains information about an email address of a user
- * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#email-info
- */
-export interface EmailInfo {
- email: string;
- preferred?: boolean;
- pending_confirmation?: boolean;
-}
-
-/**
* The CapabilityInfo entity contains information about the global capabilities of a user
* https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#capability-info
*/
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index c8e5173..1324061 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -556,12 +556,17 @@
return comment.message?.includes(USER_SUGGESTION_START_PATTERN) ?? false;
}
-export function getUserSuggestionFromString(content: string) {
- const start =
- content.indexOf(USER_SUGGESTION_START_PATTERN) +
- USER_SUGGESTION_START_PATTERN.length;
- const end = content.indexOf('\n```', start);
- return content.substring(start, end);
+export function getUserSuggestionFromString(
+ content: string,
+ suggestionIndex = 0
+) {
+ const suggestions = content.split(USER_SUGGESTION_START_PATTERN).slice(1);
+ if (suggestions.length === 0) return '';
+
+ const targetIndex = Math.min(suggestionIndex, suggestions.length - 1);
+ const targetSuggestion = suggestions[targetIndex];
+ const end = targetSuggestion.indexOf('\n```');
+ return end !== -1 ? targetSuggestion.substring(0, end) : targetSuggestion;
}
export function getUserSuggestion(comment: Comment) {
diff --git a/polygerrit-ui/app/utils/comment-util_test.ts b/polygerrit-ui/app/utils/comment-util_test.ts
index 16c8bfd..031a738 100644
--- a/polygerrit-ui/app/utils/comment-util_test.ts
+++ b/polygerrit-ui/app/utils/comment-util_test.ts
@@ -18,6 +18,7 @@
getMentionedThreads,
isNewThread,
createNew,
+ getUserSuggestionFromString,
} from './comment-util';
import {
createAccountWithEmail,
@@ -533,4 +534,69 @@
]);
});
});
+
+ suite('getUserSuggestionFromString', () => {
+ const createSuggestionContent = (suggestions: string[]) =>
+ suggestions
+ .map(s => `${USER_SUGGESTION_START_PATTERN}${s}\n\`\`\``)
+ .join('\n');
+
+ test('returns empty string for content without suggestions', () => {
+ const content = 'This is a comment without any suggestions.';
+ assert.equal(getUserSuggestionFromString(content), '');
+ });
+
+ test('returns first suggestion when no index is provided', () => {
+ const content = createSuggestionContent(['First suggestion']);
+ assert.equal(getUserSuggestionFromString(content), 'First suggestion');
+ });
+
+ test('returns correct suggestion for given index', () => {
+ const content = createSuggestionContent([
+ 'First suggestion',
+ 'Second suggestion',
+ 'Third suggestion',
+ ]);
+ assert.equal(
+ getUserSuggestionFromString(content, 1),
+ 'Second suggestion'
+ );
+ });
+
+ test('returns last suggestion when index is out of bounds', () => {
+ const content = createSuggestionContent([
+ 'First suggestion',
+ 'Second suggestion',
+ ]);
+ assert.equal(
+ getUserSuggestionFromString(content, 5),
+ 'Second suggestion'
+ );
+ });
+
+ test('handles suggestion without closing backticks', () => {
+ const content = `${USER_SUGGESTION_START_PATTERN}Unclosed suggestion`;
+ assert.equal(getUserSuggestionFromString(content), 'Unclosed suggestion');
+ });
+
+ test('handles multiple suggestions with varying content', () => {
+ const content = createSuggestionContent([
+ 'First\nMultiline\nSuggestion',
+ 'Second suggestion',
+ 'Third suggestion with `backticks`',
+ ]);
+ assert.equal(
+ getUserSuggestionFromString(content, 0),
+ 'First\nMultiline\nSuggestion'
+ );
+ assert.equal(
+ getUserSuggestionFromString(content, 1),
+ 'Second suggestion'
+ );
+ assert.equal(
+ getUserSuggestionFromString(content, 2),
+ 'Third suggestion with `backticks`'
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/utils/submit-requirement-util.ts b/polygerrit-ui/app/utils/submit-requirement-util.ts
index ff99b7f..2eec672 100644
--- a/polygerrit-ui/app/utils/submit-requirement-util.ts
+++ b/polygerrit-ui/app/utils/submit-requirement-util.ts
@@ -36,12 +36,17 @@
break;
}
searchStartIndex = index + match.length;
- // Include unary minus.
+ // Check for unary minus *before* adding the match
+ let atomIsPassing = isPassing; // Use a local variable
if (index !== 0 && text[index - 1] === '-') {
--index;
- isPassing = !isPassing;
+ atomIsPassing = !isPassing; // Negate only for this occurrence
}
- matchedAtoms.push({start: index, end: searchStartIndex, isPassing});
+ matchedAtoms.push({
+ start: index,
+ end: searchStartIndex,
+ isPassing: atomIsPassing,
+ });
}
}
diff --git a/polygerrit-ui/app/utils/submit-requirement-util_test.ts b/polygerrit-ui/app/utils/submit-requirement-util_test.ts
index a35a121..7982987 100644
--- a/polygerrit-ui/app/utils/submit-requirement-util_test.ts
+++ b/polygerrit-ui/app/utils/submit-requirement-util_test.ts
@@ -113,4 +113,42 @@
},
]);
});
+
+ test('atomizeExpression b/370742469', () => {
+ const expression: SubmitRequirementExpressionInfo = {
+ expression:
+ '-is:android-cherry-pick_exemptedusers OR is:android-cherry-pick_exemptedusers',
+ passing_atoms: [
+ 'is:android-cherry-pick_exemptedusers',
+ 'is:android-cherry-pick_exemptedusers',
+ 'project:platform/frameworks/support',
+ ],
+ failing_atoms: [
+ 'label:Code-Review=MIN',
+ 'label:Code-Review=MAX,user=non_uploader',
+ 'label:Code-Review=MAX,count>=2',
+ 'label:Code-Review=MAX',
+ 'label:Exempt=+1',
+ 'uploader:1474732',
+ 'project:platform/developers/docs',
+ ],
+ };
+
+ assert.deepStrictEqual(atomizeExpression(expression), [
+ {
+ atomStatus: SubmitRequirementExpressionAtomStatus.FAILING,
+ isAtom: true,
+ value: '-is:android-cherry-pick_exemptedusers',
+ },
+ {
+ value: ' OR ',
+ isAtom: false,
+ },
+ {
+ atomStatus: SubmitRequirementExpressionAtomStatus.PASSING,
+ isAtom: true,
+ value: 'is:android-cherry-pick_exemptedusers',
+ },
+ ]);
+ });
});
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 91caf31..39697be 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -137,18 +137,18 @@
sha1 = "cb2f351bf4463751201f43bb99865235d5ba07ca",
)
- SSHD_VERS = "2.12.0"
+ SSHD_VERS = "2.14.0"
maven_jar(
name = "sshd-osgi",
artifact = "org.apache.sshd:sshd-osgi:" + SSHD_VERS,
- sha1 = "32b8de1cbb722ba75bdf9898e0c41d42af00ce57",
+ sha1 = "6ef66228a088f8ac1383b2ff28f3102f80ebc01a",
)
maven_jar(
name = "sshd-sftp",
artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
- sha1 = "0f96f00a07b186ea62838a6a4122e8f4cad44df6",
+ sha1 = "c070ac920e72023ae9ab0a3f3a866bece284b470",
)
maven_jar(
@@ -166,7 +166,7 @@
maven_jar(
name = "sshd-mina",
artifact = "org.apache.sshd:sshd-mina:" + SSHD_VERS,
- sha1 = "8b202f7d4c0d7b714fd0c93a1352af52aa031149",
+ sha1 = "05e1293af53a196ac3c5a4b01dd88985e8672e9e",
)
maven_jar(