Merge "feat: Allow listing tags in specific order"
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 4cd5bb2..37121350 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1728,7 +1728,7 @@
This endpoint allows to trigger background reindexing of an index version. It is
also supported to specify whether to reuse existing up-to-date (non-stale) index
-documents.
+documents and whether to notifyListeners or not.
.Request
----
@@ -1736,7 +1736,8 @@
Content-Type: application/json; charset=UTF-8
{
- "reuse": "true"
+ "reuse": "true",
+ "notifyListeners": "false"
}
----
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 767e68e..efbac97 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -162,6 +162,17 @@
public abstract String metaId();
/**
+ * A unique tag which identifies the current version of the account.
+ *
+ * <p>It can be any non-empty string. For open-source gerrit it is the same as metaId; internally
+ * in google a different value is assigned.
+ *
+ * <p>The value can be null only during account updating/creation.
+ */
+ @Nullable
+ public abstract String uniqueTag();
+
+ /**
* Create a new account.
*
* @param newId unique id, see Sequences#nextAccountId().
@@ -278,6 +289,11 @@
public abstract Builder setMetaId(@Nullable String metaId);
+ @Nullable
+ public abstract String uniqueTag();
+
+ public abstract Builder setUniqueTag(@Nullable String uniqueTag);
+
public abstract Account build();
}
@@ -296,6 +312,7 @@
.add("inactive", inactive())
.add("status", status())
.add("metaId", metaId())
+ .add("uniqueTag", uniqueTag())
.toString();
}
}
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index 32b4b21..bfb4407 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -76,6 +76,16 @@
/** Indexes all entities for the provided index. */
public abstract Result indexAll(I index);
+ /**
+ * Indexes all entities for the provided index.
+ *
+ * <p>NOTE: This method does not implement the 'notifyListeners' logic which is effectively
+ * ignored and all listeners are always notified.
+ */
+ public Result indexAll(I index, @SuppressWarnings("unused") boolean notifyListeners) {
+ return indexAll(index);
+ }
+
protected final void addErrorListener(
ListenableFuture<?> future, String desc, ProgressMonitor progress, AtomicBoolean ok) {
future.addListener(
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java b/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java
index e3e485f..5584eba 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java
@@ -110,6 +110,7 @@
throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
}
account.setMetaId(id.name());
+ account.setUniqueTag(id.name());
}
return account.build();
}
diff --git a/java/com/google/gerrit/server/account/AccountProperties.java b/java/com/google/gerrit/server/account/AccountProperties.java
index 5f56aa3..9cc20d4 100644
--- a/java/com/google/gerrit/server/account/AccountProperties.java
+++ b/java/com/google/gerrit/server/account/AccountProperties.java
@@ -97,6 +97,7 @@
accountBuilder.setStatus(get(accountConfig, KEY_STATUS));
accountBuilder.setMetaId(metaId != null ? metaId.name() : null);
+ accountBuilder.setUniqueTag(accountBuilder.metaId());
account = accountBuilder.build();
}
diff --git a/java/com/google/gerrit/server/account/CachedAccountDetails.java b/java/com/google/gerrit/server/account/CachedAccountDetails.java
index e167a23..e6e2735 100644
--- a/java/com/google/gerrit/server/account/CachedAccountDetails.java
+++ b/java/com/google/gerrit/server/account/CachedAccountDetails.java
@@ -114,7 +114,8 @@
.setDisplayName(Strings.nullToEmpty(account.displayName()))
.setPreferredEmail(Strings.nullToEmpty(account.preferredEmail()))
.setStatus(Strings.nullToEmpty(account.status()))
- .setMetaId(Strings.nullToEmpty(account.metaId()));
+ .setMetaId(Strings.nullToEmpty(account.metaId()))
+ .setUniqueTag(Strings.nullToEmpty(account.uniqueTag()));
serialized.setAccount(accountProto);
for (Map.Entry<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> watch :
@@ -145,7 +146,7 @@
public CachedAccountDetails deserialize(byte[] in) {
Cache.AccountDetailsProto proto =
Protos.parseUnchecked(Cache.AccountDetailsProto.parser(), in);
- Account account =
+ Account.Builder builder =
Account.builder(
Account.id(proto.getAccount().getId()),
Instant.ofEpochMilli(proto.getAccount().getRegisteredOn()))
@@ -155,7 +156,11 @@
.setInactive(proto.getAccount().getInactive())
.setStatus(Strings.emptyToNull(proto.getAccount().getStatus()))
.setMetaId(Strings.emptyToNull(proto.getAccount().getMetaId()))
- .build();
+ .setUniqueTag(Strings.emptyToNull(proto.getAccount().getUniqueTag()));
+ if (Strings.isNullOrEmpty(builder.uniqueTag())) {
+ builder.setUniqueTag(builder.metaId());
+ }
+ Account account = builder.build();
ImmutableMap.Builder<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
projectWatches = ImmutableMap.builder();
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index ad46483..81f1e71 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -419,7 +419,7 @@
@Nullable
private BloomFilter<K> buildBloomFilter() {
SqlHandle c = null;
- try {
+ try (TraceTimer ignored = TraceContext.newTimer("Build bloom filter", Metadata.empty())) {
c = acquire();
if (estimatedSize <= 0) {
try (PreparedStatement ps =
@@ -761,6 +761,8 @@
"ALTER TABLE data ADD COLUMN IF NOT EXISTS "
+ "space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v)");
stmt.addBatch("ALTER TABLE data ADD COLUMN IF NOT EXISTS version INT DEFAULT 0 NOT NULL");
+ stmt.addBatch("CREATE INDEX IF NOT EXISTS version_key ON data(version, k)");
+ stmt.addBatch("CREATE INDEX IF NOT EXISTS accessed ON data(accessed)");
stmt.executeBatch();
}
}
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 481c17b..dc09bf4 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -251,7 +251,15 @@
private void hashAccount(Hasher h, AccountState accountState, byte[] buf) {
h.putInt(accountState.account().id().get());
- h.putString(MoreObjects.firstNonNull(accountState.account().metaId(), ZERO_ID_STRING), UTF_8);
+ // Based on the code, it seems the metaId should never be null in this place and so the
+ // uniqueTag.
+ // However, the null-check for metaId has been existed here for some time already - for safety
+ // the same check is applied to uniqueTag.
+ h.putString(
+ MoreObjects.firstNonNull(
+ accountState.account().uniqueTag(),
+ MoreObjects.firstNonNull(accountState.account().metaId(), ZERO_ID_STRING)),
+ UTF_8);
accountState.externalIds().stream().forEach(e -> hashObjectId(h, e.blobId(), buf));
}
}
diff --git a/java/com/google/gerrit/server/index/IndexVersionReindexer.java b/java/com/google/gerrit/server/index/IndexVersionReindexer.java
index 5d136e8..84be97e 100644
--- a/java/com/google/gerrit/server/index/IndexVersionReindexer.java
+++ b/java/com/google/gerrit/server/index/IndexVersionReindexer.java
@@ -35,14 +35,14 @@
}
public <K, V, I extends Index<K, V>> Future<SiteIndexer.Result> reindex(
- IndexDefinition<K, V, I> def, int version, boolean reuse) {
+ IndexDefinition<K, V, I> def, int version, boolean reuse, boolean notifyListeners) {
I index = def.getIndexCollection().getWriteIndex(version);
SiteIndexer<K, V, I> siteIndexer = def.getSiteIndexer(reuse);
return executor.submit(
() -> {
String name = def.getName();
logger.atInfo().log("Starting reindex of %s version %d", name, version);
- SiteIndexer.Result result = siteIndexer.indexAll(index);
+ SiteIndexer.Result result = siteIndexer.indexAll(index, notifyListeners);
if (result.success()) {
logger.atInfo().log("Reindex %s version %s complete", name, version);
} else {
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 925f68d..19a0223 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -177,6 +177,11 @@
@Override
public Result indexAll(ChangeIndex index) {
+ return indexAll(index, true);
+ }
+
+ @Override
+ public Result indexAll(ChangeIndex index, boolean notifyListeners) {
// The simplest approach to distribute indexing would be to let each thread grab a project
// and index it fully. But if a site has one big project and 100s of small projects, then
// in the beginning all CPUs would be busy reindexing projects. But soon enough all small
@@ -199,7 +204,7 @@
failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
List<ListenableFuture<?>> futures;
try {
- futures = new SliceScheduler(index, ok).schedule();
+ futures = new SliceScheduler(index, ok, notifyListeners).schedule();
} catch (ProjectsCollectionFailure e) {
logger.atSevere().log("%s", e.getMessage());
return Result.create(sw, false, 0, 0);
@@ -359,6 +364,7 @@
private class SliceScheduler {
final ChangeIndex index;
final AtomicBoolean ok;
+ final boolean notifyListeners;
final AtomicInteger changeCount = new AtomicInteger(0);
final AtomicInteger projectsFailed = new AtomicInteger(0);
final List<ListenableFuture<?>> sliceIndexerFutures = new ArrayList<>();
@@ -366,9 +372,10 @@
VolatileTask projTask = mpm.beginVolatileSubTask("project-slices");
Task slicingProjects;
- public SliceScheduler(ChangeIndex index, AtomicBoolean ok) {
+ public SliceScheduler(ChangeIndex index, AtomicBoolean ok, boolean notifyListeners) {
this.index = index;
this.ok = ok;
+ this.notifyListeners = notifyListeners;
}
private List<ListenableFuture<?>> schedule() throws ProjectsCollectionFailure {
@@ -376,7 +383,7 @@
int projectCount = projects.size();
slicingProjects = mpm.beginSubTask("Slicing projects", projectCount);
for (Project.NameKey name : projects) {
- sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name)));
+ sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name, notifyListeners)));
}
try {
@@ -407,9 +414,11 @@
private class ProjectSliceCreator implements Callable<Void> {
final Project.NameKey name;
+ final boolean notifyListeners;
- public ProjectSliceCreator(Project.NameKey name) {
+ public ProjectSliceCreator(Project.NameKey name, boolean notifyListeners) {
this.name = name;
+ this.notifyListeners = notifyListeners;
}
@Override
@@ -434,9 +443,10 @@
ChangeIndexer indexer;
if (reuseExistingDocuments) {
indexer =
- indexerFactory.create(executor, index, stalenessCheckerFactory.create(index));
+ indexerFactory.create(
+ executor, index, stalenessCheckerFactory.create(index), notifyListeners);
} else {
- indexer = indexerFactory.create(executor, index);
+ indexer = indexerFactory.create(executor, index, notifyListeners);
}
ListenableFuture<?> future =
executor.submit(reindexProjectSlice(indexer, projectSlice, doneTask, failedTask));
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4ec390d..fc666ad 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -71,9 +71,18 @@
ChangeIndexer create(ListeningExecutorService executor, ChangeIndex index);
ChangeIndexer create(
- ListeningExecutorService executor, ChangeIndex index, StalenessChecker stalenessChecker);
+ ListeningExecutorService executor, ChangeIndex index, boolean notifyListeners);
+
+ ChangeIndexer create(
+ ListeningExecutorService executor,
+ ChangeIndex index,
+ StalenessChecker stalenessChecker,
+ boolean notifyListeners);
ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
+
+ ChangeIndexer create(
+ ListeningExecutorService executor, ChangeIndexCollection indexes, boolean notifyListeners);
}
@Nullable private final ChangeIndexCollection indexes;
@@ -87,6 +96,7 @@
private final StalenessChecker stalenessChecker;
private final boolean autoReindexIfStale;
private final IsFirstInsertForEntry isFirstInsertForEntry;
+ private final boolean notifyListeners;
private final Map<Change.Id, IndexTask> queuedIndexTasks = new ConcurrentHashMap<>();
private final Set<ReindexIfStaleTask> queuedReindexIfStaleTasks =
@@ -104,6 +114,33 @@
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndex index,
IsFirstInsertForEntry isFirstInsertForEntry) {
+ this(
+ cfg,
+ changeDataFactory,
+ notesFactory,
+ context,
+ indexedListeners,
+ stalenessChecker,
+ batchExecutor,
+ executor,
+ index,
+ true,
+ isFirstInsertForEntry);
+ }
+
+ @AssistedInject
+ ChangeIndexer(
+ @GerritServerConfig Config cfg,
+ ChangeData.Factory changeDataFactory,
+ ChangeNotes.Factory notesFactory,
+ ThreadLocalRequestContext context,
+ PluginSetContext<ChangeIndexedListener> indexedListeners,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @Assisted ListeningExecutorService executor,
+ @Assisted ChangeIndex index,
+ @Assisted boolean notifyListeners,
+ IsFirstInsertForEntry isFirstInsertForEntry) {
this.executor = executor;
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
@@ -115,6 +152,7 @@
this.index = index;
this.indexes = null;
this.isFirstInsertForEntry = isFirstInsertForEntry;
+ this.notifyListeners = notifyListeners;
}
@AssistedInject
@@ -128,7 +166,8 @@
IsFirstInsertForEntry isFirstInsertForEntry,
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndex index,
- @Assisted StalenessChecker stalenessChecker) {
+ @Assisted StalenessChecker stalenessChecker,
+ @Assisted boolean notifyListeners) {
this.executor = executor;
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
@@ -140,6 +179,7 @@
this.index = index;
this.indexes = null;
this.stalenessChecker = stalenessChecker;
+ this.notifyListeners = notifyListeners;
}
@AssistedInject
@@ -154,6 +194,33 @@
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndexCollection indexes,
IsFirstInsertForEntry isFirstInsertForEntry) {
+ this(
+ cfg,
+ changeDataFactory,
+ notesFactory,
+ context,
+ indexedListeners,
+ stalenessChecker,
+ batchExecutor,
+ executor,
+ indexes,
+ true,
+ isFirstInsertForEntry);
+ }
+
+ @AssistedInject
+ ChangeIndexer(
+ @GerritServerConfig Config cfg,
+ ChangeData.Factory changeDataFactory,
+ ChangeNotes.Factory notesFactory,
+ ThreadLocalRequestContext context,
+ PluginSetContext<ChangeIndexedListener> indexedListeners,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @Assisted ListeningExecutorService executor,
+ @Assisted ChangeIndexCollection indexes,
+ @Assisted boolean notifyListeners,
+ IsFirstInsertForEntry isFirstInsertForEntry) {
this.executor = executor;
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
@@ -164,6 +231,7 @@
this.autoReindexIfStale = autoReindexIfStale(cfg);
this.index = null;
this.indexes = indexes;
+ this.notifyListeners = notifyListeners;
this.isFirstInsertForEntry = isFirstInsertForEntry;
}
@@ -290,19 +358,27 @@
}
private void fireChangeScheduledForIndexingEvent(String projectName, int id) {
- indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+ }
}
private void fireChangeIndexedEvent(String projectName, int id) {
- indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
+ }
}
private void fireChangeScheduledForDeletionFromIndexEvent(int id) {
- indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+ }
}
private void fireChangeDeletedFromIndexEvent(int id) {
- indexedListeners.runEach(l -> l.onChangeDeleted(id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeDeleted(id));
+ }
}
/**
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 5b66277..aa2c297 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.project;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.Streams;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.exceptions.StorageException;
@@ -156,7 +156,7 @@
// Skip evaluating the default submit rule if the project has prolog rules.
// Note that in this case, the prolog submit rule will handle labels for us
.filter(
- projectState.hasPrologRules()
+ projectState.hasPrologRules() && prologSubmitRuleUtil.isProjectRulesEnabled()
? rule -> !(rule.get() instanceof DefaultSubmitRule)
: rule -> true)
.map(
@@ -185,14 +185,10 @@
*/
public SubmitTypeRecord getSubmitType(ChangeData cd) {
try (Timer0.Context ignored = metrics.submitTypeEvaluationLatency.start()) {
- try {
- Project.NameKey name = cd.project();
- Optional<ProjectState> project = projectCache.get(name);
- if (!project.isPresent()) {
- throw new NoSuchProjectException(name);
- }
- } catch (NoSuchProjectException e) {
- throw new IllegalStateException("Unable to find project while evaluating submit rule", e);
+ ProjectState projectState =
+ projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
+ if (!prologSubmitRuleUtil.isProjectRulesEnabled()) {
+ return SubmitTypeRecord.OK(projectState.getSubmitType());
}
return prologSubmitRuleUtil.getSubmitType(cd);
diff --git a/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java b/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
index d923155..21cd1c1 100644
--- a/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
+++ b/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
@@ -25,7 +25,8 @@
public class ReindexIndexVersion implements RestModifyView<IndexVersionResource, Input> {
public static class Input {
- boolean reuse;
+ public boolean reuse;
+ public boolean notifyListeners;
}
private final IndexVersionReindexer indexVersionReindexer;
@@ -41,7 +42,7 @@
IndexDefinition<?, ?, ?> def = rsrc.getIndexDefinition();
int version = rsrc.getIndex().getSchema().getVersion();
@SuppressWarnings("unused")
- var unused = indexVersionReindexer.reindex(def, version, input.reuse);
+ var unused = indexVersionReindexer.reindex(def, version, input.reuse, input.notifyListeners);
return Response.accepted(
String.format("Index %s version %d submitted for reindexing", def.getName(), version));
}
diff --git a/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java b/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java
index a94fb6e..a568371 100644
--- a/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java
+++ b/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java
@@ -20,22 +20,29 @@
/** Provides prolog-related operations to different callers. */
public interface PrologSubmitRuleUtil {
+ /** Returns true if prolog rules are enabled for the project. */
+ boolean isProjectRulesEnabled();
/**
* Returns the submit-type of a change depending on the change data and the definition of the
* prolog rules file.
+ *
+ * <p>Must only be called when Prolog rules are enabled on the Gerrit server.
*/
SubmitTypeRecord getSubmitType(ChangeData cd);
/**
* Returns the submit-type of a change depending on the change data and the definition of the
* prolog rules file.
+ *
+ * <p>Must only be called when Prolog rules are enabled on the Gerrit server.
*/
SubmitTypeRecord getSubmitType(ChangeData cd, String ruleToTest, boolean skipFilters);
- /** Evaluates a submit rule. */
+ /**
+ * Evaluates a submit rule.
+ *
+ * <p>Must only be called when Prolog rules are enabled on the Gerrit server.
+ */
SubmitRecord evaluate(ChangeData cd, String ruleToTest, boolean skipFilters);
-
- /** Returns true if prolog rules are enabled for the project. */
- boolean isProjectRulesEnabled();
}
diff --git a/java/com/google/gerrit/server/rules/prolog/PrologRule.java b/java/com/google/gerrit/server/rules/prolog/PrologRule.java
index 13814bb..c560fd2 100644
--- a/java/com/google/gerrit/server/rules/prolog/PrologRule.java
+++ b/java/com/google/gerrit/server/rules/prolog/PrologRule.java
@@ -30,15 +30,22 @@
class PrologRule implements SubmitRule {
private final PrologRuleEvaluator.Factory factory;
private final ProjectCache projectCache;
+ private final boolean isProjectRulesEnabled;
@Inject
- private PrologRule(PrologRuleEvaluator.Factory factory, ProjectCache projectCache) {
+ private PrologRule(
+ PrologRuleEvaluator.Factory factory, ProjectCache projectCache, RulesCache rulesCache) {
this.factory = factory;
this.projectCache = projectCache;
+ this.isProjectRulesEnabled = rulesCache.isProjectRulesEnabled();
}
@Override
public Optional<SubmitRecord> evaluate(ChangeData cd) {
+ if (!isProjectRulesEnabled) {
+ return Optional.empty();
+ }
+
ProjectState projectState =
projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
// We only want to run the Prolog engine if we have at least one rules.pl file to use.
@@ -49,15 +56,11 @@
return Optional.of(evaluate(cd, PrologOptions.defaultOptions()));
}
- public SubmitRecord evaluate(ChangeData cd, PrologOptions opts) {
+ SubmitRecord evaluate(ChangeData cd, PrologOptions opts) {
return getEvaluator(cd, opts).evaluate();
}
- public SubmitTypeRecord getSubmitType(ChangeData cd) {
- return getSubmitType(cd, PrologOptions.defaultOptions());
- }
-
- public SubmitTypeRecord getSubmitType(ChangeData cd, PrologOptions opts) {
+ SubmitTypeRecord getSubmitType(ChangeData cd, PrologOptions opts) {
return getEvaluator(cd, opts).getSubmitType();
}
diff --git a/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java b/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
index 3d017e2..6be71f8 100644
--- a/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
+++ b/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.rules.prolog;
+import static com.google.common.base.Preconditions.checkState;
+
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.server.query.change.ChangeData;
@@ -25,7 +27,6 @@
@Singleton
public class PrologSubmitRuleUtilImpl implements PrologSubmitRuleUtil {
private final PrologRule prologRule;
-
private final RulesCache rulesCache;
@Inject
@@ -35,22 +36,25 @@
}
@Override
+ public boolean isProjectRulesEnabled() {
+ return rulesCache.isProjectRulesEnabled();
+ }
+
+ @Override
public SubmitTypeRecord getSubmitType(ChangeData cd) {
- return prologRule.getSubmitType(cd);
+ checkState(isProjectRulesEnabled(), "prolog rules disabled");
+ return prologRule.getSubmitType(cd, PrologOptions.defaultOptions());
}
@Override
public SubmitTypeRecord getSubmitType(ChangeData cd, String ruleToTest, boolean skipFilters) {
+ checkState(isProjectRulesEnabled(), "prolog rules disabled");
return prologRule.getSubmitType(cd, PrologOptions.dryRunOptions(ruleToTest, skipFilters));
}
@Override
public SubmitRecord evaluate(ChangeData cd, String ruleToTest, boolean skipFilters) {
+ checkState(isProjectRulesEnabled(), "prolog rules disabled");
return prologRule.evaluate(cd, PrologOptions.dryRunOptions(ruleToTest, skipFilters));
}
-
- @Override
- public boolean isProjectRulesEnabled() {
- return rulesCache.isProjectRulesEnabled();
- }
}
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index 2c01548..330b602 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -43,6 +43,7 @@
return newState(
Account.builder(accountId, TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
+ .setUniqueTag("1234567812345678123456781234567812345678")
.build());
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 6f3f8d7..fdf7457 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -2089,23 +2089,30 @@
}
@Test
- public void checkMetaId() throws Exception {
- // metaId is set when account is loaded
+ public void checkMetaIdAndUniqueTag() throws Exception {
+ // In open-source Gerrit, the uniqueTag and metaId are always the same. Check them together
+ // in this test.
+ // metaId and uniqueTag are set when account is loaded
assertThat(accounts.get(admin.id()).get().account().metaId()).isEqualTo(getMetaId(admin.id()));
+ assertThat(accounts.get(admin.id()).get().account().uniqueTag())
+ .isEqualTo(getMetaId(admin.id()));
- // metaId is set when account is created
+ // metaId and uniqueTag are set when account is created
AccountsUpdate au = accountsUpdateProvider.get();
Account.Id accountId = Account.id(seq.nextAccountId());
AccountState accountState = au.insert("Create Test Account", accountId, u -> {});
assertThat(accountState.account().metaId()).isEqualTo(getMetaId(accountId));
+ assertThat(accountState.account().uniqueTag()).isEqualTo(getMetaId(accountId));
- // metaId is set when account is updated
+ // metaId and uniqueTag are set when account is updated
Optional<AccountState> updatedAccountState =
au.update("Set Full Name", accountId, u -> u.setFullName("foo"));
assertThat(updatedAccountState).isPresent();
Account updatedAccount = updatedAccountState.get().account();
assertThat(accountState.account().metaId()).isNotEqualTo(updatedAccount.metaId());
+ assertThat(accountState.account().uniqueTag()).isNotEqualTo(updatedAccount.uniqueTag());
assertThat(updatedAccount.metaId()).isEqualTo(getMetaId(accountId));
+ assertThat(updatedAccount.uniqueTag()).isEqualTo(getMetaId(accountId));
}
private EmailInput newEmailInput(String email, boolean noConfirmation) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index d1d3620..83de9824 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -28,8 +28,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.entities.Change;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -37,14 +36,12 @@
import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import com.google.gerrit.testing.ConfigSuite;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -60,7 +57,7 @@
return submitWholeTopicEnabledConfig();
}
- private class RulesPl extends VersionedMetaData {
+ private static class RulesPl extends VersionedMetaData {
private String rule;
@Override
@@ -69,21 +66,12 @@
}
@Override
- protected void onLoad() throws IOException, ConfigInvalidException {
+ protected void onLoad() throws IOException {
rule = readUTF8(RULES_PL_FILE);
}
@Override
- protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
- TestSubmitRuleInput in = new TestSubmitRuleInput();
- in.rule = rule;
- try {
- @SuppressWarnings("unused")
- var unused = gApi.changes().id(testChangeId.get()).current().testSubmitType(in);
- } catch (RestApiException e) {
- throw new ConfigInvalidException("Invalid submit type rule", e);
- }
-
+ protected boolean onSave(CommitBuilder commit) throws IOException {
saveUTF8(RULES_PL_FILE, rule);
return true;
}
@@ -91,18 +79,10 @@
private AtomicInteger fileCounter;
- // The change is used only to verify that the rule is valid. It is never submitted in the test.
- private Change.Id testChangeId;
-
@Before
public void setUp() throws Exception {
fileCounter = new AtomicInteger();
gApi.projects().name(project.get()).branch("test").create(new BranchInput());
- Result testChange = createChange("test", "test change");
- testChangeId = testChange.getChange().getId();
- // Reset repo back to the original state - otherwise all changes in tests have testChange as a
- // parent.
- testRepo.reset(testChange.getCommit().getParent(0));
}
private void setRulesPl(String rule) throws Exception {
@@ -193,6 +173,21 @@
}
@Test
+ @GerritConfig(name = "rules.enable", value = "false")
+ public void submitType_rulesTakeNoEffectWhenDisabled() throws Exception {
+ PushOneCommit.Result r1 = createChange("master", "Default 1");
+ PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2");
+ PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
+
+ setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+ // Rules take no effect
+ assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
+ }
+
+ @Test
public void submitTypeIsUsedForSubmit() throws Exception {
setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
diff --git a/javatests/com/google/gerrit/acceptance/server/index/change/BUILD b/javatests/com/google/gerrit/acceptance/server/index/BUILD
similarity index 81%
rename from javatests/com/google/gerrit/acceptance/server/index/change/BUILD
rename to javatests/com/google/gerrit/acceptance/server/index/BUILD
index 3be5249..1d4ef02 100644
--- a/javatests/com/google/gerrit/acceptance/server/index/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/index/BUILD
@@ -1,7 +1,7 @@
load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
acceptance_tests(
- srcs = glob(["*IT.java"]),
+ srcs = glob(["**/*IT.java"]),
group = "server_index",
labels = ["server"],
)
diff --git a/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java b/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java
new file mode 100644
index 0000000..d433ca7
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java
@@ -0,0 +1,87 @@
+// 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.
+
+package com.google.gerrit.acceptance.server.index;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.gerrit.server.restapi.config.ReindexIndexVersion;
+import com.google.inject.Inject;
+import java.util.Collection;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ReindexIndexVersionIT extends AbstractDaemonTest {
+
+ @Inject private ReindexIndexVersion reindexIndexVersion;
+ @Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ private IndexDefinition<?, ?, ?> def;
+ private Index<?, ?> changeIndex;
+ private Change.Id C1;
+ private Change.Id C2;
+
+ private ChangeIndexedListener changeIndexedListener;
+ private ReindexIndexVersion.Input input = new ReindexIndexVersion.Input();
+
+ @Before
+ public void setUp() throws Exception {
+ def = indexDefs.stream().filter(i -> i.getName().equals("changes")).findFirst().get();
+ changeIndex = def.getIndexCollection().getSearchIndex();
+ C1 = createChange().getChange().getId();
+ C2 = createChange().getChange().getId();
+ changeIndexedListener = mock(ChangeIndexedListener.class);
+ input = new ReindexIndexVersion.Input();
+ }
+
+ @Test
+ public void reindexWithListenerNotification() throws Exception {
+ input.notifyListeners = true;
+ reindex();
+ verify(changeIndexedListener, times(1)).onChangeIndexed(project.get(), C1.get());
+ verify(changeIndexedListener, times(1)).onChangeIndexed(project.get(), C2.get());
+ }
+
+ @Test
+ public void reindexWithoutListenerNotification() throws Exception {
+ input.notifyListeners = false;
+ reindex();
+ verifyNoInteractions(changeIndexedListener);
+ }
+
+ private void reindex() throws ResourceNotFoundException {
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(changeIndexedListener)) {
+ Response<?> rsp =
+ reindexIndexVersion.apply(new IndexVersionResource(def, changeIndex), input);
+ assertThat(rsp.statusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java
index 1f47958..772812f 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java
@@ -22,6 +22,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.RefNames;
@@ -170,6 +171,29 @@
assertThat(statusForRuleAddFile("foo")).isEqualTo(SubmitRecord.Status.RULE_ERROR);
}
+ @Test
+ @GerritConfig(name = "rules.enable", value = "false")
+ public void prologRule_noEffectWhenRulesDisabled() throws Exception {
+ modifySubmitRules("gerrit:commit_message_matches('foo.*')");
+ String changeId = createChange().getChangeId();
+ // Default rules don't allow submission
+ assertThat(gApi.changes().id(changeId).get().submittable).isFalse();
+ // Satisfy default rules
+ approve(changeId);
+
+ assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
+ }
+
+ @Test
+ @GerritConfig(name = "rules.enable", value = "true")
+ public void prologRule_takesEffectWhenRulesEnabled() throws Exception {
+ modifySubmitRules("gerrit:commit_message_matches('foo.*')");
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ assertThat(gApi.changes().id(changeId).get().submittable).isFalse();
+ }
+
private SubmitRecord.Status statusForRule() throws Exception {
String oldHead = projectOperations.project(project).getHead("master").name();
PushOneCommit.Result result =
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index 46027a2..f726be3 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -103,6 +103,7 @@
Account account =
Account.builder(Account.id(1), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
+ .setUniqueTag("1234567812345678123456781234567812345678")
.build();
Account.Id ownerId = account.id();
diff --git a/javatests/com/google/gerrit/server/account/AccountCacheTest.java b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
index 6628362..45eff9a 100644
--- a/javatests/com/google/gerrit/server/account/AccountCacheTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
@@ -41,12 +41,15 @@
@Test
public void account_roundTrip() throws Exception {
+ // The uniqueTag and metaId can be different (in google internal implementation).
+ // This tests ensures that they are serialized/deserialized separately.
Account account =
Account.builder(Account.id(1), Instant.EPOCH)
.setFullName("foo bar")
.setDisplayName("foo")
.setActive(false)
.setMetaId("dead..beef")
+ .setUniqueTag("dead..beef..tag")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld")
.build();
@@ -63,6 +66,7 @@
.setDisplayName("foo")
.setInactive(true)
.setMetaId("dead..beef")
+ .setUniqueTag("dead..beef..tag")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld"))
.build();
@@ -71,6 +75,39 @@
}
@Test
+ public void account_deserializeOldRecordWithoutUniqueTag() throws Exception {
+ Account.Builder builder =
+ Account.builder(Account.id(1), Instant.EPOCH)
+ .setFullName("foo bar")
+ .setDisplayName("foo")
+ .setActive(false)
+ .setMetaId("dead..beef")
+ .setStatus("OOO")
+ .setPreferredEmail("foo@bar.tld");
+ CachedAccountDetails original =
+ CachedAccountDetails.create(builder.build(), ImmutableMap.of(), CachedPreferences.EMPTY);
+ CachedAccountDetails expected =
+ CachedAccountDetails.create(
+ builder.setUniqueTag("dead..beef").build(), ImmutableMap.of(), CachedPreferences.EMPTY);
+ byte[] serialized = SERIALIZER.serialize(original);
+ Cache.AccountDetailsProto expectedProto =
+ Cache.AccountDetailsProto.newBuilder()
+ .setAccount(
+ Cache.AccountProto.newBuilder()
+ .setId(1)
+ .setRegisteredOn(0)
+ .setFullName("foo bar")
+ .setDisplayName("foo")
+ .setInactive(true)
+ .setMetaId("dead..beef")
+ .setStatus("OOO")
+ .setPreferredEmail("foo@bar.tld"))
+ .build();
+ ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expectedProto);
+ Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(expected);
+ }
+
+ @Test
public void account_roundTripNullFields() throws Exception {
CachedAccountDetails original =
CachedAccountDetails.create(ACCOUNT, ImmutableMap.of(), CachedPreferences.EMPTY);
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 34f746a..a53abfa 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -351,6 +351,7 @@
return AccountState.forAccount(
Account.builder(Account.id(id), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
+ .setUniqueTag("1234567812345678123456781234567812345678")
.build());
}
diff --git a/plugins/replication b/plugins/replication
index 2f6c7ce..012f042 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 2f6c7ceeb0cc50bc73d018cd9f990392d58804ab
+Subproject commit 012f04240eafe6dfa21fd94e012e97498881c621
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index dded0c6..37a17ba 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -146,6 +146,7 @@
GENERATE_SUGGESTION_ENABLED = 'generate_suggestion_enabled',
// User disabled generating suggestions
GENERATE_SUGGESTION_DISABLED = 'generate_suggestion_disabled',
+ GENERATE_SUGGESTION_EDITED = 'generate_suggestion_edited',
START_REVIEW = 'start-review',
CODE_REVIEW_APPROVAL = 'code-review-approval',
FILE_LIST_DIFF_COLLAPSED = 'file-list-diff-collapsed',
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index df04f68..68fbe8b 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -33,7 +33,12 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
import {testResolver} from '../../../test/common-test-setup';
-import {commentsModelToken} from '../../../models/comments/comments-model';
+import {TEST_PROJECT_NAME} from '../../../test/test-data-generators';
+import {
+ ChangeChildView,
+ changeViewModelToken,
+} from '../../../models/views/change';
+import {GerritView} from '../../../services/router/router-model';
const author = {
_account_id: 42 as AccountId,
@@ -138,9 +143,12 @@
element = await fixture<GrMessagesList>(
html`<gr-messages-list></gr-messages-list>`
);
- await testResolver(commentsModelToken).reloadComments(
- 0 as NumericChangeId
- );
+ testResolver(changeViewModelToken).setState({
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
+ changeNum: 123 as NumericChangeId,
+ repo: TEST_PROJECT_NAME,
+ });
element.messages = messages;
await element.updateComplete;
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 1a18e9c..0bf49db 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -98,10 +98,7 @@
} from '../../../utils/attention-set-util';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import {resolve} from '../../../models/dependency';
-import {
- changeModelToken,
- updateRevisionsWithCommitShas,
-} from '../../../models/change/change-model';
+import {changeModelToken} from '../../../models/change/change-model';
import {LabelNameToValuesMap, PatchSetNumber} from '../../../api/rest-api';
import {css, html, PropertyValues, LitElement, nothing} from 'lit';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -1462,10 +1459,8 @@
return this.saveReview(reviewInput, errFn)
.then(result => {
this.getChangeModel().updateStateChange(
- updateRevisionsWithCommitShas(
- GrReviewerUpdatesParser.parse(
- result?.change_info as ChangeViewChangeInfo
- )
+ GrReviewerUpdatesParser.parse(
+ result?.change_info as ChangeViewChangeInfo
)
);
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 16a783c..48d7b1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -232,6 +232,9 @@
generatedSuggestionId?: string;
@state()
+ addedGeneratedSuggestion?: string;
+
+ @state()
suggestionsProvider?: SuggestionsProvider;
@state()
@@ -1149,9 +1152,10 @@
private handleAddGeneratedSuggestion(code: string) {
const addNewLine = this.messageText.length !== 0;
- this.messageText += `${
+ this.addedGeneratedSuggestion = `${
addNewLine ? '\n' : ''
}${USER_SUGGESTION_START_PATTERN}${code}${'\n```'}`;
+ this.messageText += this.addedGeneratedSuggestion;
}
private generateSuggestEdit() {
@@ -1665,6 +1669,7 @@
} else {
// No need to make a backend call when nothing has changed.
while (this.somethingToSave()) {
+ this.trackGeneratedSuggestionEdit();
this.comment = await this.rawSave({showToast: true});
if (isError(this.comment)) return;
}
@@ -1746,6 +1751,19 @@
);
this.closeDeleteCommentModal();
}
+
+ private trackGeneratedSuggestionEdit() {
+ const wasGeneratedSuggestionEdited =
+ this.addedGeneratedSuggestion &&
+ !this.messageText.includes(this.addedGeneratedSuggestion);
+ if (wasGeneratedSuggestionEdited) {
+ this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_EDITED, {
+ uuid: this.generatedSuggestionId,
+ commentId: this.comment?.id ?? '',
+ });
+ this.addedGeneratedSuggestion = undefined;
+ }
+ }
}
declare global {
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 2414107..bba7e47 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -19,7 +19,13 @@
} from '../../types/common';
import {ChangeStatus, DefaultBase} from '../../constants/constants';
import {combineLatest, from, Observable, forkJoin, of} from 'rxjs';
-import {map, filter, withLatestFrom, switchMap} from 'rxjs/operators';
+import {
+ map,
+ filter,
+ withLatestFrom,
+ switchMap,
+ catchError,
+} from 'rxjs/operators';
import {
computeAllPatchSets,
computeLatestPatchNum,
@@ -55,7 +61,7 @@
export interface ChangeState {
/**
* If `change` is undefined, this must be either NOT_LOADED or LOADING.
- * If `change` is defined, this must be either LOADED or RELOADING.
+ * If `change` is defined, this must be either LOADED.
*/
loadingStatus: LoadingStatus;
change?: ParsedChangeInfo;
@@ -177,7 +183,13 @@
};
export const changeModelToken = define<ChangeModel>('change-model');
-
+/**
+ * Change model maintains information about the current change.
+ *
+ * The "current" change is defined by ChangeViewModel. This model tracks part of
+ * the current view. As such it's a singleton global state. It's NOT meant to
+ * keep the state of an arbitrary change.
+ */
export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
@@ -199,8 +211,7 @@
public readonly loading$ = select(
this.changeLoadingStatus$,
- status =>
- status === LoadingStatus.LOADING || status === LoadingStatus.RELOADING
+ status => status === LoadingStatus.LOADING
);
public readonly reviewedFiles$ = select(
@@ -388,10 +399,7 @@
private reportChangeReload() {
return this.changeLoadingStatus$.subscribe(loadingStatus => {
- if (
- loadingStatus === LoadingStatus.LOADING ||
- loadingStatus === LoadingStatus.RELOADING
- ) {
+ if (loadingStatus === LoadingStatus.LOADING) {
this.reporting.time(Timing.CHANGE_RELOAD);
}
if (
@@ -557,16 +565,21 @@
return this.viewModel.changeNum$
.pipe(
switchMap(changeNum => {
- if (changeNum !== undefined) this.updateStateLoading(changeNum);
- const change = from(this.restApiService.getChangeDetail(changeNum));
- const edit = from(this.restApiService.getChangeEdit(changeNum));
+ this.updateStateLoading(changeNum);
+ // if changeNum is undefined restApi calls return undefined.
+ const change = this.restApiService.getChangeDetail(changeNum);
+ const edit = this.restApiService.getChangeEdit(changeNum);
return forkJoin([change, edit]);
}),
withLatestFrom(this.viewModel.patchNum$),
map(([[change, edit], patchNum]) =>
updateChangeWithEdit(change, edit, patchNum)
),
- map(updateRevisionsWithCommitShas)
+ catchError(err => {
+ // Reset loading state and re-throw.
+ this.updateState({loadingStatus: LoadingStatus.NOT_LOADED});
+ throw err;
+ })
)
.subscribe(change => {
// The change service is currently a singleton, so we have to be
@@ -752,25 +765,33 @@
/**
* Called when change detail loading is initiated.
*
- * If the change number matches the current change in the state, then
- * this is a reload. If not, then we not just want to set the state to
- * LOADING instead of RELOADING, but we also want to set the change to
+ * We want to set the state to LOADING, but we also want to set the change to
* undefined right away. Otherwise components could see inconsistent state:
* a new change number, but an old change.
*/
- private updateStateLoading(changeNum: NumericChangeId) {
- const current = this.getState();
- const reloading = current.change?._number === changeNum;
+ private updateStateLoading(changeNum?: NumericChangeId) {
this.updateState({
- change: reloading ? current.change : undefined,
- loadingStatus: reloading
- ? LoadingStatus.RELOADING
- : LoadingStatus.LOADING,
+ change: undefined,
+ loadingStatus: changeNum
+ ? LoadingStatus.LOADING
+ : LoadingStatus.NOT_LOADED,
});
}
// Private but used in tests.
+ /**
+ * Update the change information in the state.
+ *
+ * Since the ChangeModel must maintain consistency with ChangeViewModel
+ * The update is only allowed, if the new change has the same number as the
+ * current change or if the current change is not set (it was reset to
+ * undefined when ChangeViewModel.changeNum updated).
+ */
updateStateChange(change?: ParsedChangeInfo) {
+ if (this.change && change?._number !== this.change?._number) {
+ return;
+ }
+ change = updateRevisionsWithCommitShas(change);
this.updateState({
change,
loadingStatus:
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index f074ac3..ebe6066 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -306,11 +306,11 @@
// Reloading same change
document.dispatchEvent(new CustomEvent('reload'));
- state = await waitForLoadingStatus(LoadingStatus.RELOADING);
+ state = await waitForLoadingStatus(LoadingStatus.LOADING);
assert.equal(stub.callCount, 3);
assert.equal(stub.getCall(1).firstArg, undefined);
assert.equal(stub.getCall(2).firstArg, TEST_NUMERIC_CHANGE_ID);
- assert.deepEqual(state?.change, updateRevisionsWithCommitShas(knownChange));
+ assert.deepEqual(state?.change, undefined);
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index 767e604..a8eda0f 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -30,12 +30,16 @@
} from '../../test/test-data-generators';
import {waitUntil, waitUntilCalled} from '../../test/test-utils';
import {ParsedChangeInfo} from '../../types/types';
-import {changeModelToken} from '../change/change-model';
+import {
+ changeModelToken,
+ updateRevisionsWithCommitShas,
+} from '../change/change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {changeViewModelToken} from '../views/change';
import {NumericChangeId, PatchSetNumber} from '../../api/rest-api';
import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
+import {deepEqual} from '../../utils/deep-util';
const PLUGIN_NAME = 'test-plugin';
@@ -106,17 +110,17 @@
});
await waitUntil(() => change === undefined);
- const testChange = createParsedChange();
+ const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
- await waitUntil(() => change === testChange);
+ await waitUntil(() => deepEqual(change, testChange));
await waitUntilCalled(fetchSpy, 'fetch');
assert.equal(
model.latestPatchNum,
- testChange.revisions[testChange.current_revision]
+ testChange!.revisions[testChange!.current_revision]
._number as PatchSetNumber
);
- assert.equal(model.changeNum, testChange._number);
+ assert.equal(model.changeNum, testChange!._number);
});
test('fetch throttle', async () => {
@@ -133,9 +137,9 @@
});
await waitUntil(() => change === undefined);
- const testChange = createParsedChange();
+ const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
- await waitUntil(() => change === testChange);
+ await waitUntil(() => deepEqual(change, testChange));
model.reload('test-plugin');
model.reload('test-plugin');
@@ -339,9 +343,9 @@
});
await waitUntil(() => change === undefined);
clock.tick(1);
- const testChange = createParsedChange();
+ const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
- await waitUntil(() => change === testChange);
+ await waitUntil(() => deepEqual(change, testChange));
clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
const pollCount = fetchSpy.callCount;
@@ -366,9 +370,9 @@
});
await waitUntil(() => change === undefined);
clock.tick(1);
- const testChange = createParsedChange();
+ const testChange = updateRevisionsWithCommitShas(createParsedChange());
testResolver(changeModelToken).updateStateChange(testChange);
- await waitUntil(() => change === testChange);
+ await waitUntil(() => deepEqual(change, testChange));
clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
clock.tick(1);
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index f26a565..41b47ef 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -7,7 +7,6 @@
import {
CommentInfo,
NumericChangeId,
- PatchSetNum,
RevisionId,
UrlEncodedCommentId,
RobotCommentInfo,
@@ -34,7 +33,14 @@
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
import {define} from '../dependency';
-import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
+import {
+ BehaviorSubject,
+ combineLatest,
+ forkJoin,
+ from,
+ Observable,
+ of,
+} from 'rxjs';
import {fire, fireAlert} from '../../utils/event-util';
import {CURRENT} from '../../utils/patch-set-util';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
@@ -69,9 +75,20 @@
drafts?: {[path: string]: DraftInfo[]};
// Ported comments only affect `CommentThread` properties, not individual
// comments.
- /** undefined means 'still loading' */
+ /**
+ * Comments ported from earlier patchsets.
+ *
+ * This only considers current patchset (right side), not the base patchset
+ * (left-side).
+ *
+ * undefined means 'still loading'
+ */
portedComments?: {[path: string]: CommentInfo[]};
- /** undefined means 'still loading' */
+ /**
+ * Drafts ported from earlier patchsets.
+ *
+ * undefined means 'still loading'
+ */
portedDrafts?: {[path: string]: DraftInfo[]};
/**
* If a draft is discarded by the user, then we temporarily keep it in this
@@ -216,7 +233,7 @@
return nextState;
}
-/** Adds or updates a draft. */
+/** Adds or updates a draft in the state. */
export function setDraft(state: CommentState, draft: DraftInfo): CommentState {
const nextState = {...state};
assert(!!draft.path, 'draft without path');
@@ -235,6 +252,12 @@
return nextState;
}
+/** Removes a draft from the state.
+ *
+ * Removed draft is stored in discardedDrafts for potential undo operation.
+ * discardedDrafts however is only a client-side cache and such drafts are not
+ * retained in the server.
+ */
export function deleteDraft(
state: CommentState,
draft: DraftInfo
@@ -254,6 +277,10 @@
}
export const commentsModelToken = define<CommentsModel>('comments-model');
+/**
+ * Model that maintains the state of all comments and drafts for the current
+ * change in the context of change-view.
+ */
export class CommentsModel extends Model<CommentState> {
public readonly commentsLoading$ = select(
this.state$,
@@ -415,6 +442,8 @@
}
);
+ public readonly reloadAllComments$ = new BehaviorSubject(undefined);
+
public thread$(id: UrlEncodedCommentId) {
return select(this.threads$, threads => threads.find(t => t.rootId === id));
}
@@ -423,8 +452,6 @@
private changeNum?: NumericChangeId;
- private patchNum?: PatchSetNum;
-
private drafts: {[path: string]: DraftInfo[]} = {};
private draftToastTask?: DelayedTask;
@@ -464,9 +491,8 @@
this.subscriptions.push(
this.drafts$.subscribe(x => (this.drafts = x ?? {}))
);
- this.subscriptions.push(
- this.changeModel.patchNum$.subscribe(x => (this.patchNum = x))
- );
+ // Patchset-level draft should always exist when opening reply dialog.
+ // If there are none, create an empty one.
this.subscriptions.push(
combineLatest([
this.draftsLoading$,
@@ -478,41 +504,55 @@
})
);
this.subscriptions.push(
- this.changeViewModel.changeNum$.subscribe(changeNum => {
- this.changeNum = changeNum;
- this.setState({...initialState});
- this.reloadAllComments();
- })
+ combineLatest([this.changeViewModel.changeNum$, this.reloadAllComments$])
+ .pipe(
+ switchMap(([changeNum, _]) => {
+ this.changeNum = changeNum;
+ this.setState({...initialState});
+ if (!changeNum) return of([undefined, undefined, undefined]);
+ return forkJoin([
+ this.restApiService.getDiffComments(changeNum),
+ this.restApiService.getDiffRobotComments(changeNum),
+ this.restApiService.getDiffDrafts(changeNum),
+ ]);
+ })
+ )
+ .subscribe(([comments, robotComments, drafts]) => {
+ this.reportRobotCommentStats(robotComments);
+ this.modifyState(s => {
+ s = setComments(s, comments);
+ s = setRobotComments(s, robotComments);
+ return setDrafts(s, drafts);
+ });
+ })
);
+ // When the patchset selection changes update information about comments
+ // ported from earlier patchsets.
this.subscriptions.push(
- combineLatest([
- this.changeModel.changeNum$,
- this.changeModel.patchNum$,
- ]).subscribe(([changeNum, patchNum]) => {
- this.changeNum = changeNum;
- this.patchNum = patchNum;
- this.reloadAllPortedComments();
- })
+ combineLatest([this.changeModel.changeNum$, this.changeModel.patchNum$])
+ .pipe(
+ switchMap(([changeNum, patchNum]) => {
+ this.changeNum = changeNum;
+ if (!changeNum) return of([undefined, undefined]);
+ const revision = patchNum ?? (CURRENT as RevisionId);
+ return forkJoin([
+ this.restApiService.getPortedComments(changeNum, revision),
+ this.restApiService.getPortedDrafts(changeNum, revision),
+ ]);
+ })
+ )
+ .subscribe(([portedComments, portedDrafts]) =>
+ this.modifyState(s => {
+ s = setPortedComments(s, portedComments);
+ return setPortedDrafts(s, portedDrafts);
+ })
+ )
);
}
// Note that this does *not* reload ported comments.
- async reloadAllComments() {
- if (!this.changeNum) return;
- await Promise.all([
- this.reloadComments(this.changeNum),
- this.reloadRobotComments(this.changeNum),
- this.reloadDrafts(this.changeNum),
- ]);
- }
-
- async reloadAllPortedComments() {
- if (!this.changeNum) return;
- if (!this.patchNum) return;
- await Promise.all([
- this.reloadPortedComments(this.changeNum, this.patchNum),
- this.reloadPortedDrafts(this.changeNum, this.patchNum),
- ]);
+ reloadAllComments() {
+ this.reloadAllComments$.next(undefined);
}
// visible for testing
@@ -520,19 +560,6 @@
this.setState(reducer({...this.getState()}));
}
- async reloadComments(changeNum: NumericChangeId): Promise<void> {
- const comments = await this.restApiService.getDiffComments(changeNum);
- this.modifyState(s => setComments(s, comments));
- }
-
- async reloadRobotComments(changeNum: NumericChangeId): Promise<void> {
- const robotComments = await this.restApiService.getDiffRobotComments(
- changeNum
- );
- this.reportRobotCommentStats(robotComments);
- this.modifyState(s => setRobotComments(s, robotComments));
- }
-
private reportRobotCommentStats(obj?: PathToRobotCommentsInfoMap) {
if (!obj) return;
const comments = Object.values(obj).flat();
@@ -561,33 +588,6 @@
);
}
- async reloadDrafts(changeNum: NumericChangeId): Promise<void> {
- const drafts = await this.restApiService.getDiffDrafts(changeNum);
- this.modifyState(s => setDrafts(s, drafts));
- }
-
- async reloadPortedComments(
- changeNum: NumericChangeId,
- patchNum = CURRENT as RevisionId
- ): Promise<void> {
- const portedComments = await this.restApiService.getPortedComments(
- changeNum,
- patchNum
- );
- this.modifyState(s => setPortedComments(s, portedComments));
- }
-
- async reloadPortedDrafts(
- changeNum: NumericChangeId,
- patchNum = CURRENT as RevisionId
- ): Promise<void> {
- const portedDrafts = await this.restApiService.getPortedDrafts(
- changeNum,
- patchNum
- );
- this.modifyState(s => setPortedDrafts(s, portedDrafts));
- }
-
async restoreDraft(draftId: UrlEncodedCommentId) {
const found = this.discardedDrafts?.find(d => id(d) === draftId);
if (!found) throw new Error('discarded draft not found');
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 9ab0f64..bdfb870 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,5 +1,5 @@
-load("//tools/bzl:genrule2.bzl", "genrule2")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
+load("//tools/bzl:genrule2.bzl", "genrule2")
def polygerrit_bundle(name, srcs, outs, entry_point, app_name):
"""Build .zip bundle from source code
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 43e06a4..f48588b 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -41,7 +41,6 @@
export enum LoadingStatus {
NOT_LOADED = 'NOT_LOADED',
LOADING = 'LOADING',
- RELOADING = 'RELOADING',
LOADED = 'LOADED',
}
diff --git a/proto/cache.proto b/proto/cache.proto
index e4273bf..09c99df 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -355,6 +355,7 @@
bool inactive = 6;
string status = 7;
string meta_id = 8;
+ string unique_tag = 9;
}
// Serialized form of com.google.gerrit.server.account.CachedAccountDetails.Key.
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 9e515e5..5bacec8 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,6 +1,10 @@
+"""
+Build rules for plugins.
+"""
+
+load("//:version.bzl", "GERRIT_VERSION")
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//:version.bzl", "GERRIT_VERSION")
IN_TREE_BUILD_MODE = True
diff --git a/tools/util.py b/tools/util.py
index 947e2c0..aed9e91 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -16,8 +16,10 @@
REPO_ROOTS = {
'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
+ 'ECLIPSE_EGIT': 'https://repo.eclipse.org/content/repositories/egit-releases',
'GERRIT': 'https://gerrit-maven.storage.googleapis.com',
'GERRIT_API': 'https://gerrit-api.commondatastorage.googleapis.com/release',
+ 'JENKINS': 'https://repo.jenkins-ci.org/artifactory/public',
'MAVEN_CENTRAL': 'https://repo1.maven.org/maven2',
'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
'MAVEN_SNAPSHOT': 'https://oss.sonatype.org/content/repositories/snapshots',