Merge "Submit Requirements - render quick label info"
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
index 4f2f2f6..b7fa398 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -64,6 +64,13 @@
}
}
+ /** Returns true if the submit requirement is fulfilled and can allow change submission. */
+ @Memoized
+ public boolean fulfilled() {
+ Status s = status();
+ return s == Status.SATISFIED || s == Status.OVERRIDDEN || s == Status.NOT_APPLICABLE;
+ }
+
public static Builder builder() {
return new AutoValue_SubmitRequirementResult.Builder();
}
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 3d3603f..ba9f6d6 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -450,6 +450,10 @@
/**
* Get NoteDb draft refs for a change.
*
+ * <p>This is just a simple ref scan, so the results may potentially include refs for zombie draft
+ * comments. A zombie draft is one which has been published but the write to delete the draft ref
+ * from All-Users failed.
+ *
* @param changeId change ID.
* @return raw refs from All-Users repo.
*/
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 4cb080a..695997a 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -54,6 +54,7 @@
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevWalk;
/**
@@ -134,8 +135,9 @@
PatchSet.Id psId,
ChangeKind kind,
LabelType type,
- @Nullable Map<String, FileDiffOutput> modifiedFiles,
- @Nullable Map<String, FileDiffOutput> modifiedFilesLastPatchset) {
+ @Nullable Map<String, FileDiffOutput> baseVsCurrentDiff,
+ @Nullable Map<String, FileDiffOutput> baseVsPriorDiff,
+ @Nullable Map<String, FileDiffOutput> priorVsCurrentDiff) {
int n = psa.key().patchSetId().get();
checkArgument(n != psId.get());
@@ -185,7 +187,8 @@
project.getName());
return true;
} else if (type.isCopyAllScoresIfListOfFilesDidNotChange()
- && listOfFilesUnchangedPredicate.match(modifiedFiles, modifiedFilesLastPatchset)) {
+ && listOfFilesUnchangedPredicate.match(
+ baseVsCurrentDiff, baseVsPriorDiff, priorVsCurrentDiff)) {
logger.atFine().log(
"approval %d on label %s of patch set %d of change %d can be copied"
+ " to patch set %d because the label has set "
@@ -402,8 +405,9 @@
priorPatchSet.getValue().id().changeId(),
changeKind);
- Map<String, FileDiffOutput> modifiedFiles = null;
- Map<String, FileDiffOutput> modifiedFilesLastPatchSet = null;
+ Map<String, FileDiffOutput> baseVsCurrent = null;
+ Map<String, FileDiffOutput> baseVsPrior = null;
+ Map<String, FileDiffOutput> priorVsCurrent = null;
LabelTypes labelTypes = project.getLabelTypes();
for (PatchSetApproval psa : priorApprovals) {
if (resultByUser.contains(psa.label(), psa.accountId())) {
@@ -411,11 +415,13 @@
}
Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
// Only compute modified files if there is a relevant label, since this is expensive.
- if (modifiedFiles == null
+ if (baseVsCurrent == null
&& type.isPresent()
&& type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
- modifiedFiles = listModifiedFiles(project, patchSet);
- modifiedFilesLastPatchSet = listModifiedFiles(project, priorPatchSet.getValue());
+ baseVsCurrent = listModifiedFiles(project, patchSet);
+ baseVsPrior = listModifiedFiles(project, priorPatchSet.getValue());
+ priorVsCurrent =
+ listModifiedFiles(project, priorPatchSet.getValue().commitId(), patchSet.commitId());
}
if (!type.isPresent()) {
logger.atFine().log(
@@ -435,8 +441,9 @@
patchSet.id(),
changeKind,
type.get(),
- modifiedFiles,
- modifiedFilesLastPatchSet)
+ baseVsCurrent,
+ baseVsPrior,
+ priorVsCurrent)
&& !canCopyBasedOnCopyCondition(notes, psa, patchSet, type.get(), changeKind)) {
continue;
}
@@ -465,4 +472,21 @@
ex);
}
}
+
+ /**
+ * Gets the modified files between two commits corresponding to different patchsets of the same
+ * change.
+ */
+ private Map<String, FileDiffOutput> listModifiedFiles(
+ ProjectState project, ObjectId sourceCommit, ObjectId targetCommit) {
+ try {
+ return diffOperations.listModifiedFiles(project.getNameKey(), sourceCommit, targetCommit);
+ } catch (DiffNotAvailableException ex) {
+ throw new StorageException(
+ "failed to compute difference in files, so won't copy"
+ + " votes on labels even if list of files is the same and "
+ + "copyAllIfListOfFilesDidNotChange",
+ ex);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index f2034af..2d9b014 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -32,6 +32,7 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
@@ -522,7 +523,12 @@
public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments(
Account.Id author, @Nullable Ref ref) {
loadDraftComments(author, ref);
- return draftCommentNotes.getComments();
+ // Filter out any zombie draft comments. These are drafts that are also in
+ // the published map, and arise when the update to All-Users to delete them
+ // during the publish operation failed.
+ return ImmutableListMultimap.copyOf(
+ Multimaps.filterEntries(
+ draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key)));
}
public ImmutableListMultimap<ObjectId, RobotComment> getRobotComments() {
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 3cbe546..e940b1e 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -543,12 +543,16 @@
Matcher assigneeDeletedMatcher = ASSIGNEE_DELETED_PATTERN.matcher(originalChangeMessage);
if (assigneeDeletedMatcher.matches()) {
if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeDeletedMatcher.group(1)).matches()) {
+ Optional<String> assigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ oldAssignee,
+ getAccountInfoFromNameEmail(assigneeDeletedMatcher.group(1)));
+
return Optional.of(
- "Assignee deleted: "
- + getPossibleAccountReplacement(
- changeFixProgress,
- oldAssignee,
- ParsedAccountInfo.create(assigneeDeletedMatcher.group(1))));
+ assigneeReplacement.isPresent()
+ ? "Assignee deleted: " + assigneeReplacement.get()
+ : "Assignee was deleted.");
}
return Optional.empty();
}
@@ -556,12 +560,15 @@
Matcher assigneeAddedMatcher = ASSIGNEE_ADDED_PATTERN.matcher(originalChangeMessage);
if (assigneeAddedMatcher.matches()) {
if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeAddedMatcher.group(1)).matches()) {
+ Optional<String> assigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ newAssignee,
+ getAccountInfoFromNameEmail(assigneeAddedMatcher.group(1)));
return Optional.of(
- "Assignee added: "
- + getPossibleAccountReplacement(
- changeFixProgress,
- newAssignee,
- ParsedAccountInfo.create(assigneeAddedMatcher.group(1))));
+ assigneeReplacement.isPresent()
+ ? "Assignee added: " + assigneeReplacement.get()
+ : "Assignee was added.");
}
return Optional.empty();
}
@@ -569,17 +576,22 @@
Matcher assigneeChangedMatcher = ASSIGNEE_CHANGED_PATTERN.matcher(originalChangeMessage);
if (assigneeChangedMatcher.matches()) {
if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeChangedMatcher.group(1)).matches()) {
+ Optional<String> oldAssigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ oldAssignee,
+ getAccountInfoFromNameEmail(assigneeChangedMatcher.group(1)));
+ Optional<String> newAssigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ newAssignee,
+ getAccountInfoFromNameEmail(assigneeChangedMatcher.group(2)));
return Optional.of(
- String.format(
- "Assignee changed from: %s to: %s",
- getPossibleAccountReplacement(
- changeFixProgress,
- oldAssignee,
- ParsedAccountInfo.create(assigneeChangedMatcher.group(1))),
- getPossibleAccountReplacement(
- changeFixProgress,
- newAssignee,
- ParsedAccountInfo.create(assigneeChangedMatcher.group(2)))));
+ oldAssigneeReplacement.isPresent() && newAssigneeReplacement.isPresent()
+ ? String.format(
+ "Assignee changed from: %s to: %s",
+ oldAssigneeReplacement.get(), newAssigneeReplacement.get())
+ : "Assignee was changed.");
}
return Optional.empty();
}
@@ -610,12 +622,15 @@
Matcher matcher = REMOVED_VOTE_PATTERN.matcher(originalChangeMessage);
if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
- return Optional.of(
- String.format(
- "Removed %s by %s",
- matcher.group(1),
- getPossibleAccountReplacement(
- changeFixProgress, reviewer, getAccountInfoFromNameEmail(matcher.group(2)))));
+ Optional<String> reviewerReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress, reviewer, getAccountInfoFromNameEmail(matcher.group(2)));
+ StringBuilder replacement = new StringBuilder();
+ replacement.append("Removed ").append(matcher.group(1));
+ if (reviewerReplacement.isPresent()) {
+ replacement.append(" by ").append(reviewerReplacement.get());
+ }
+ return Optional.of(replacement.toString());
}
return Optional.empty();
}
@@ -637,14 +652,14 @@
String replacementLine = lines[i];
if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
anyFixed = true;
- replacementLine =
- String.format(
- "* %s by %s\n",
- matcher.group(1),
- getPossibleAccountReplacement(
- changeFixProgress,
- Optional.empty(),
- getAccountInfoFromNameEmail(matcher.group(2))));
+ Optional<String> reviewerReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress, Optional.empty(), getAccountInfoFromNameEmail(matcher.group(2)));
+ replacementLine = "* " + matcher.group(1);
+ if (reviewerReplacement.isPresent()) {
+ replacementLine += " by " + reviewerReplacement.get();
+ }
+ replacementLine += "\n";
}
fixedLines.append(replacementLine);
}
@@ -708,11 +723,14 @@
StringBuffer sb = new StringBuffer();
while (onAddReviewerMatcher.find()) {
String reviewerName = normalizeOnCodeOwnerAddReviewerMatch(onAddReviewerMatcher.group(1));
- String replacementName =
+ Optional<String> replacementName =
getPossibleAccountReplacement(
changeFixProgress, Optional.empty(), ParsedAccountInfo.create(reviewerName));
onAddReviewerMatcher.appendReplacement(
- sb, replacementName + ", who was added as reviewer owns the following files");
+ sb,
+ replacementName.isPresent()
+ ? replacementName.get() + ", who was added as reviewer owns the following files"
+ : "Added reviewer owns the following files");
}
onAddReviewerMatcher.appendTail(sb);
sb.append("\n");
@@ -1071,19 +1089,20 @@
* <p>If {@code account} is known, replace with {@link AccountTemplateUtil#getAccountTemplate}.
* Otherwise, try to guess the correct replacement account for {@code accountName} among {@link
* ChangeFixProgress#parsedAccounts} that appeared in the change. If this fails {@link
- * #DEFAULT_ACCOUNT_REPLACEMENT} is applied.
+ * Optional#empty} is returned.
*
* @param changeFixProgress see {@link ChangeFixProgress}
* @param account account that should be used for replacement, if known
* @param accountInfo {@link ParsedAccountInfo} to replace.
- * @return replacement for {@code accountName}
+ * @return replacement for {@code accountName} or {@link Optional#empty}, if the replacement could
+ * not be determined.
*/
- private String getPossibleAccountReplacement(
+ private Optional<String> getPossibleAccountReplacement(
ChangeFixProgress changeFixProgress,
Optional<Account.Id> account,
ParsedAccountInfo accountInfo) {
if (account.isPresent()) {
- return AccountTemplateUtil.getAccountTemplate(account.get());
+ return Optional.of(AccountTemplateUtil.getAccountTemplate(account.get()));
}
// Retrieve reviewer accounts from cache and try to match by their name.
Map<Account.Id, AccountState> missingAccountStateReviewers =
@@ -1129,7 +1148,7 @@
e.getValue().get().account().getName(), accountInfo.name()))
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> e.getValue().get()));
}
- String replacementName = DEFAULT_ACCOUNT_REPLACEMENT;
+ Optional<String> replacementName = Optional.empty();
if (possibleReplacements.isEmpty()) {
logger.atWarning().log(
"Fixing ref %s, could not find reviewer account matching name %s",
@@ -1140,8 +1159,9 @@
changeFixProgress.changeMetaRef, accountInfo);
} else {
replacementName =
- AccountTemplateUtil.getAccountTemplate(
- Iterables.getOnlyElement(possibleReplacements.keySet()));
+ Optional.of(
+ AccountTemplateUtil.getAccountTemplate(
+ Iterables.getOnlyElement(possibleReplacements.keySet())));
}
return replacementName;
}
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
index 57a3cd7..1a7d5af 100644
--- a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -50,7 +50,9 @@
// patchset to the user before it was merged.
ChangeData changeData = changeDataFactory.create(ctx.getProject(), ctx.getChange().getId());
ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- update.putSubmitRequirementResults(evaluator.evaluateAllRequirements(changeData).values());
+ // We do not want to store submit requirements in NoteDb for legacy submit records
+ update.putSubmitRequirementResults(
+ evaluator.evaluateAllRequirements(changeData, /* includeLegacy= */ false).values());
return !changeData.submitRequirements().isEmpty();
}
}
diff --git a/java/com/google/gerrit/server/patch/DiffExecutor.java b/java/com/google/gerrit/server/patch/DiffExecutor.java
index 63d5c50..c9b87ff 100644
--- a/java/com/google/gerrit/server/patch/DiffExecutor.java
+++ b/java/com/google/gerrit/server/patch/DiffExecutor.java
@@ -16,14 +16,11 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.google.gerrit.server.patch.filediff.PatchListLoader;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
import java.util.concurrent.ExecutorService;
-/**
- * Marker on {@link ExecutorService} used by {@link IntraLineLoader} and {@link PatchListLoader}.
- */
+/** Marker on {@link ExecutorService} used by {@link IntraLineLoader}. */
@Retention(RUNTIME)
@BindingAnnotation
public @interface DiffExecutor {}
diff --git a/java/com/google/gerrit/server/patch/PatchListCache.java b/java/com/google/gerrit/server/patch/PatchListCache.java
index 6d42249..b8651e0 100644
--- a/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -16,18 +16,11 @@
import com.google.gerrit.entities.Project;
-/** Provides a cached list of {@link PatchListEntry}. */
+/**
+ * Provides a cached list of intra-line and summary diffs. Use {@link DiffOperations} to compute
+ * detailed file diffs.
+ */
public interface PatchListCache {
- /**
- * Returns the patch list - list of modified files - between two commits.
- *
- * @param key identifies the old / new commits.
- * @param project name key identifying a specific git project (repository).
- * @return patch list containing the modified files between two commits.
- * @deprecated use {@link DiffOperations} instead.
- */
- @Deprecated
- PatchList get(PatchListKey key, Project.NameKey project) throws PatchListNotAvailableException;
IntraLineDiff getIntraLineDiff(IntraLineDiffKey key, IntraLineDiffArgs args);
diff --git a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index dd2bb47..eab0c22 100644
--- a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,14 +15,12 @@
package com.google.gerrit.server.patch;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
+import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.patch.filediff.PatchListLoader;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -30,12 +28,12 @@
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
/** Provides a cached list of {@link PatchListEntry}. */
@Singleton
public class PatchListCacheImpl implements PatchListCache {
- public static final String FILE_NAME = "diff";
+ public static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
static final String INTRA_NAME = "diff_intraline";
static final String DIFF_SUMMARY = "diff_summary";
@@ -43,13 +41,6 @@
return new CacheModule() {
@Override
protected void configure() {
- factory(PatchListLoader.Factory.class);
- // TODO(davido): Switch off using legacy cache backend, after fixing PatchListLoader
- // to be recursion free.
- persist(FILE_NAME, PatchListKey.class, PatchList.class, CacheBackend.GUAVA)
- .maximumWeight(10 << 20)
- .weigher(PatchListWeigher.class);
-
factory(IntraLineLoader.Factory.class);
persist(INTRA_NAME, IntraLineDiffKey.class, IntraLineDiff.class)
.maximumWeight(10 << 20)
@@ -67,27 +58,21 @@
};
}
- private final Cache<PatchListKey, PatchList> fileCache;
private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
private final Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
- private final PatchListLoader.Factory fileLoaderFactory;
private final IntraLineLoader.Factory intraLoaderFactory;
private final DiffSummaryLoader.Factory diffSummaryLoaderFactory;
private final boolean computeIntraline;
@Inject
PatchListCacheImpl(
- @Named(FILE_NAME) Cache<PatchListKey, PatchList> fileCache,
@Named(INTRA_NAME) Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
@Named(DIFF_SUMMARY) Cache<DiffSummaryKey, DiffSummary> diffSummaryCache,
- PatchListLoader.Factory fileLoaderFactory,
IntraLineLoader.Factory intraLoaderFactory,
DiffSummaryLoader.Factory diffSummaryLoaderFactory,
@GerritServerConfig Config cfg) {
- this.fileCache = fileCache;
this.intraCache = intraCache;
this.diffSummaryCache = diffSummaryCache;
- this.fileLoaderFactory = fileLoaderFactory;
this.intraLoaderFactory = intraLoaderFactory;
this.diffSummaryLoaderFactory = diffSummaryLoaderFactory;
@@ -97,31 +82,6 @@
}
@Override
- public PatchList get(PatchListKey key, Project.NameKey project)
- throws PatchListNotAvailableException {
- try {
- PatchList pl = fileCache.get(key, fileLoaderFactory.create(key, project));
- if (pl instanceof LargeObjectTombstone) {
- throw new PatchListObjectTooLargeException(
- "Error computing " + key + ". Previous attempt failed with LargeObjectException");
- }
- return pl;
- } catch (ExecutionException e) {
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
- throw new PatchListNotAvailableException(e);
- } catch (UncheckedExecutionException e) {
- if (e.getCause() instanceof LargeObjectException) {
- // Cache negative result so we don't need to redo expensive computations that would yield
- // the same result.
- fileCache.put(key, new LargeObjectTombstone());
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
- throw new PatchListNotAvailableException(e);
- }
- throw e;
- }
- }
-
- @Override
public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key, IntraLineDiffArgs args) {
if (computeIntraline) {
try {
@@ -140,28 +100,14 @@
try {
return diffSummaryCache.get(key, diffSummaryLoaderFactory.create(key, project));
} catch (ExecutionException e) {
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
+ logger.atWarning().withCause(e).log("Error computing %s", key);
throw new PatchListNotAvailableException(e);
} catch (UncheckedExecutionException e) {
if (e.getCause() instanceof LargeObjectException) {
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
+ logger.atWarning().withCause(e).log("Error computing %s", key);
throw new PatchListNotAvailableException(e);
}
throw e;
}
}
-
- /** Used to cache negative results in {@code fileCache}. */
- @VisibleForTesting
- public static class LargeObjectTombstone extends PatchList {
- private static final long serialVersionUID = 1L;
-
- @VisibleForTesting
- public LargeObjectTombstone() {
- // Initialize super class with valid values. We don't care about the inner state, but need to
- // pass valid values that don't break (de)serialization.
- super(
- null, ObjectId.zeroId(), false, ComparisonType.againstAutoMerge(), new PatchListEntry[0]);
- }
- }
}
diff --git a/java/com/google/gerrit/server/patch/PatchListWeigher.java b/java/com/google/gerrit/server/patch/PatchListWeigher.java
deleted file mode 100644
index 942d0e0..0000000
--- a/java/com/google/gerrit/server/patch/PatchListWeigher.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.patch;
-
-import com.google.common.cache.Weigher;
-
-/** Approximates memory usage for PatchList in bytes of memory used. */
-public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
- @Override
- public int weigh(PatchListKey key, PatchList value) {
- int size =
- 16
- + 4 * 8
- + 2 * 36
- + 8 // Size of PatchListKey, 64 bit JVM
- + 16
- + 3 * 8
- + 3 * 4
- + 20; // Size of PatchList, 64 bit JVM
- for (PatchListEntry e : value.getPatches()) {
- size += e.weigh();
- }
- return size;
- }
-}
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 0b08c4f..33300e3 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -71,28 +71,8 @@
intralineDiffCalculator = calculator;
}
- /** Convert into {@link PatchScript} using the old diff cache output. */
- PatchScript toPatchScriptOld(Repository git, PatchList list, PatchListEntry content)
- throws IOException {
-
- PatchFileChange change =
- new PatchFileChange(
- content.getEdits(),
- content.getEditsDueToRebase(),
- content.getHeaderLines(),
- content.getOldName(),
- content.getNewName(),
- content.getChangeType(),
- content.getPatchType());
- SidesResolver sidesResolver = new SidesResolver(git, list.getComparisonType());
- ResolvedSides sides =
- resolveSides(
- git, sidesResolver, oldName(change), newName(change), list.getOldId(), list.getNewId());
- return build(sides.a, sides.b, change);
- }
-
/** Convert into {@link PatchScript} using the new diff cache output. */
- PatchScript toPatchScriptNew(Repository git, FileDiffOutput content) throws IOException {
+ PatchScript toPatchScript(Repository git, FileDiffOutput content) throws IOException {
PatchFileChange change =
new PatchFileChange(
content.edits().stream().map(TaggedEdit::jgitEdit).collect(toImmutableList()),
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 96b23c8..02f125a 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -229,7 +229,7 @@
notes.getProjectName(), bId, parentNum, fileName, diffPrefs.ignoreWhitespace)
: diffOperations.getModifiedFile(
notes.getProjectName(), aId, bId, fileName, diffPrefs.ignoreWhitespace);
- return newBuilder().toPatchScriptNew(git, fileDiffOutput);
+ return newBuilder().toPatchScript(git, fileDiffOutput);
}
private Optional<ObjectId> getAId() {
diff --git a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java b/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
deleted file mode 100644
index 031f3db..0000000
--- a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
+++ /dev/null
@@ -1,705 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-//
-//
-
-package com.google.gerrit.server.patch.filediff;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.toSet;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Multimap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.metrics.Counter0;
-import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.InMemoryInserter;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.patch.AutoMerger;
-import com.google.gerrit.server.patch.ComparisonType;
-import com.google.gerrit.server.patch.DiffExecutor;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListCacheImpl;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.Text;
-import com.google.gerrit.server.patch.filediff.EditTransformer.ContextAwareEdit;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffEntry.ChangeType;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.EditList;
-import org.eclipse.jgit.diff.HistogramDiff;
-import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.diff.RawTextComparator;
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
-import org.eclipse.jgit.patch.FileHeader;
-import org.eclipse.jgit.patch.FileHeader.PatchType;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
-
-public class PatchListLoader implements Callable<PatchList> {
- public static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- public interface Factory {
- PatchListLoader create(PatchListKey key, Project.NameKey project);
- }
-
- @Singleton
- static class Metrics {
- final Counter0 timeouts;
-
- @Inject
- Metrics(MetricMaker metricMaker) {
- // TODO(ghareeb): Remove this metric from documentation once this class is deprecated.
- timeouts =
- metricMaker.newCounter(
- "caches/diff/legacy/timeouts",
- new Description(
- "Total number of git file diff computations that resulted in timeouts.")
- .setRate()
- .setUnit("count"));
- }
- }
-
- private final GitRepositoryManager repoManager;
- private final PatchListCache patchListCache;
- private final ThreeWayMergeStrategy mergeStrategy;
- private final ExecutorService diffExecutor;
- private final AutoMerger autoMerger;
- private final PatchListKey key;
- private final Metrics metrics;
- private final Project.NameKey project;
- private final long timeoutMillis;
-
- @Inject
- PatchListLoader(
- GitRepositoryManager mgr,
- PatchListCache plc,
- @GerritServerConfig Config cfg,
- @DiffExecutor ExecutorService de,
- AutoMerger am,
- Metrics metrics,
- @Assisted PatchListKey k,
- @Assisted Project.NameKey p) {
- this.repoManager = mgr;
- this.patchListCache = plc;
- this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
- this.diffExecutor = de;
- this.autoMerger = am;
- this.metrics = metrics;
- this.key = k;
- this.project = p;
- this.timeoutMillis =
- ConfigUtil.getTimeUnit(
- cfg,
- "cache",
- PatchListCacheImpl.FILE_NAME,
- "timeout",
- TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
- TimeUnit.MILLISECONDS);
- }
-
- @Override
- public PatchList call() throws IOException, PatchListNotAvailableException {
- try (Repository repo = repoManager.openRepository(project);
- InMemoryInserter ins = new InMemoryInserter(repo);
- ObjectReader reader = ins.newReader();
- RevWalk rw = new RevWalk(reader)) {
- return readPatchList(repo, rw, ins);
- }
- }
-
- private static RawTextComparator comparatorFor(Whitespace ws) {
- switch (ws) {
- case IGNORE_ALL:
- return RawTextComparator.WS_IGNORE_ALL;
-
- case IGNORE_TRAILING:
- return RawTextComparator.WS_IGNORE_TRAILING;
-
- case IGNORE_LEADING_AND_TRAILING:
- return RawTextComparator.WS_IGNORE_CHANGE;
-
- case IGNORE_NONE:
- default:
- return RawTextComparator.DEFAULT;
- }
- }
-
- private PatchList readPatchList(Repository repo, RevWalk rw, InMemoryInserter ins)
- throws IOException, PatchListNotAvailableException {
- ObjectReader reader = rw.getObjectReader();
- checkArgument(reader.getCreatedFromInserter() == ins);
- RawTextComparator cmp = comparatorFor(key.getWhitespace());
- try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- RevCommit b = rw.parseCommit(key.getNewId());
- RevObject a = aFor(key, repo, rw, ins, b);
-
- if (a == null) {
- // TODO(sop) Remove this case.
- // This is an octopus merge commit which should be compared against the
- // auto-merge. However since we don't support computing the auto-merge
- // for octopus merge commits, we fall back to diffing against the first
- // parent, even though this wasn't what was requested.
- //
- ComparisonType comparisonType = ComparisonType.againstParent(1);
- PatchListEntry[] entries = new PatchListEntry[2];
- entries[0] = newCommitMessage(cmp, reader, null, b);
- entries[1] = newMergeList(cmp, reader, null, b, comparisonType);
- return new PatchList(a, b, true, comparisonType, entries);
- }
-
- ComparisonType comparisonType = getComparisonType(a, b);
-
- RevCommit aCommit = a instanceof RevCommit ? (RevCommit) a : null;
- RevTree aTree = rw.parseTree(a);
- RevTree bTree = b.getTree();
-
- df.setReader(reader, repo.getConfig());
- df.setDiffComparator(cmp);
- df.setDetectRenames(true);
- List<DiffEntry> diffEntries = df.scan(aTree, bTree);
-
- EditsDueToRebaseResult editsDueToRebaseResult =
- determineEditsDueToRebase(aCommit, b, diffEntries, df, rw);
- diffEntries = editsDueToRebaseResult.getRelevantOriginalDiffEntries();
- Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath =
- editsDueToRebaseResult.getEditsDueToRebasePerFilePath();
-
- List<PatchListEntry> entries = new ArrayList<>();
- entries.add(
- newCommitMessage(
- cmp, reader, comparisonType.isAgainstParentOrAutoMerge() ? null : aCommit, b));
- boolean isMerge = b.getParentCount() > 1;
- if (isMerge) {
- entries.add(
- newMergeList(
- cmp,
- reader,
- comparisonType.isAgainstParentOrAutoMerge() ? null : aCommit,
- b,
- comparisonType));
- }
- for (DiffEntry diffEntry : diffEntries) {
- Set<ContextAwareEdit> editsDueToRebase =
- getEditsDueToRebase(editsDueToRebasePerFilePath, diffEntry);
- Optional<PatchListEntry> patchListEntry =
- getPatchListEntry(reader, df, diffEntry, aTree, bTree, editsDueToRebase);
- patchListEntry.ifPresent(entries::add);
- }
- return new PatchList(
- a, b, isMerge, comparisonType, entries.toArray(new PatchListEntry[entries.size()]));
- }
- }
-
- /**
- * Identifies the edits which are present between {@code commitA} and {@code commitB} due to other
- * commits in between those two. Edits which cannot be clearly attributed to those other commits
- * (because they overlap with modifications introduced by {@code commitA} or {@code commitB}) are
- * omitted from the result. The edits are expressed as differences between {@code treeA} of {@code
- * commitA} and {@code treeB} of {@code commitB}.
- *
- * <p><b>Note:</b> If one of the commits is a merge commit, an empty {@code Multimap} will be
- * returned.
- *
- * <p><b>Warning:</b> This method assumes that commitA and commitB are either a parent and child
- * commit or represent two patch sets which belong to the same change. No checks are made to
- * confirm this assumption! Passing arbitrary commits to this method may lead to strange results
- * or take very long.
- *
- * <p>This logic could be expanded to arbitrary commits if the following adjustments were applied:
- *
- * <ul>
- * <li>If {@code commitA} is an ancestor of {@code commitB} (or the other way around), {@code
- * commitA} (or {@code commitB}) is used instead of its parent in this method.
- * <li>Special handling for merge commits is added. If only one of them is a merge commit, the
- * whole computation has to be done between the single parent and all parents of the merge
- * commit. If both of them are merge commits, all combinations of parents have to be
- * considered. Alternatively, we could decide to not support this feature for merge commits
- * (or just for specific types of merge commits).
- * </ul>
- *
- * @param commitA the commit defining {@code treeA}
- * @param commitB the commit defining {@code treeB}
- * @param diffEntries the list of {@code DiffEntries} for the diff between {@code commitA} and
- * {@code commitB}
- * @param df the {@code DiffFormatter}
- * @param rw the current {@code RevWalk}
- * @return an aggregated result of the computation
- * @throws PatchListNotAvailableException if the edits can't be identified
- * @throws IOException if an error occurred while accessing the repository
- */
- private EditsDueToRebaseResult determineEditsDueToRebase(
- RevCommit commitA,
- RevCommit commitB,
- List<DiffEntry> diffEntries,
- DiffFormatter df,
- RevWalk rw)
- throws PatchListNotAvailableException, IOException {
- if (commitA == null
- || isRootOrMergeCommit(commitA)
- || isRootOrMergeCommit(commitB)
- || areParentChild(commitA, commitB)
- || haveCommonParent(commitA, commitB)) {
- return EditsDueToRebaseResult.create(diffEntries, ImmutableMultimap.of());
- }
-
- PatchListKey oldKey = PatchListKey.againstDefaultBase(key.getOldId(), key.getWhitespace());
- PatchList oldPatchList = patchListCache.get(oldKey, project);
- PatchListKey newKey = PatchListKey.againstDefaultBase(key.getNewId(), key.getWhitespace());
- PatchList newPatchList = patchListCache.get(newKey, project);
-
- List<PatchListEntry> oldPatches = oldPatchList.getPatches();
- List<PatchListEntry> newPatches = newPatchList.getPatches();
- // TODO(aliceks): Have separate but more limited lists for parents and patch sets (but don't
- // mess up renames/copies).
- Set<String> touchedFilePaths = new HashSet<>();
- for (PatchListEntry patchListEntry : oldPatches) {
- touchedFilePaths.addAll(getTouchedFilePaths(patchListEntry));
- }
- for (PatchListEntry patchListEntry : newPatches) {
- touchedFilePaths.addAll(getTouchedFilePaths(patchListEntry));
- }
-
- List<DiffEntry> relevantDiffEntries =
- diffEntries.stream()
- .filter(diffEntry -> isTouched(touchedFilePaths, diffEntry))
- .collect(toImmutableList());
-
- RevCommit parentCommitA = commitA.getParent(0);
- rw.parseBody(parentCommitA);
- RevCommit parentCommitB = commitB.getParent(0);
- rw.parseBody(parentCommitB);
- List<DiffEntry> parentDiffEntries = df.scan(parentCommitA, parentCommitB);
- // TODO(aliceks): Find a way to not construct a PatchListEntry as it contains many unnecessary
- // details and we don't fill all of them properly.
- List<PatchListEntry> parentPatchListEntries =
- getRelevantPatchListEntries(
- parentDiffEntries, parentCommitA, parentCommitB, touchedFilePaths, df);
-
- EditTransformer editTransformer = new EditTransformer(toFileEditsList(parentPatchListEntries));
- editTransformer.transformReferencesOfSideA(toFileEditsList(oldPatches));
- editTransformer.transformReferencesOfSideB(toFileEditsList(newPatches));
- return EditsDueToRebaseResult.create(
- relevantDiffEntries, editTransformer.getEditsPerFilePath());
- }
-
- private ImmutableList<FileEdits> toFileEditsList(List<PatchListEntry> entries) {
- return entries.stream().map(PatchListLoader::toFileEdits).collect(toImmutableList());
- }
-
- private static FileEdits toFileEdits(PatchListEntry patchListEntry) {
- Optional<String> oldName = Optional.empty();
- Optional<String> newName = Optional.empty();
- switch (patchListEntry.getChangeType()) {
- case DELETED:
- oldName = Optional.of(patchListEntry.getNewName());
- break;
- case ADDED:
- case MODIFIED:
- case REWRITE:
- newName = Optional.of(patchListEntry.getNewName());
- break;
-
- case COPIED:
- case RENAMED:
- oldName = Optional.of(patchListEntry.getOldName());
- newName = Optional.of(patchListEntry.getNewName());
- break;
- }
- return FileEdits.createFromJgitEdits(patchListEntry.getEdits(), oldName, newName);
- }
-
- private static boolean isRootOrMergeCommit(RevCommit commit) {
- return commit.getParentCount() != 1;
- }
-
- private static boolean areParentChild(RevCommit commitA, RevCommit commitB) {
- return ObjectId.isEqual(commitA.getParent(0), commitB)
- || ObjectId.isEqual(commitB.getParent(0), commitA);
- }
-
- private static boolean haveCommonParent(RevCommit commitA, RevCommit commitB) {
- return ObjectId.isEqual(commitA.getParent(0), commitB.getParent(0));
- }
-
- private static Set<String> getTouchedFilePaths(PatchListEntry patchListEntry) {
- String oldFilePath = patchListEntry.getOldName();
- String newFilePath = patchListEntry.getNewName();
-
- return oldFilePath == null
- ? ImmutableSet.of(newFilePath)
- : ImmutableSet.of(oldFilePath, newFilePath);
- }
-
- private static boolean isTouched(Set<String> touchedFilePaths, DiffEntry diffEntry) {
- String oldFilePath = diffEntry.getOldPath();
- String newFilePath = diffEntry.getNewPath();
- // One of the above file paths could be /dev/null but we need not explicitly check for this
- // value as the set of file paths shouldn't contain it.
- return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
- }
-
- private List<PatchListEntry> getRelevantPatchListEntries(
- List<DiffEntry> parentDiffEntries,
- RevCommit parentCommitA,
- RevCommit parentCommitB,
- Set<String> touchedFilePaths,
- DiffFormatter diffFormatter)
- throws IOException {
- List<PatchListEntry> parentPatchListEntries = new ArrayList<>(parentDiffEntries.size());
- for (DiffEntry parentDiffEntry : parentDiffEntries) {
- if (!isTouched(touchedFilePaths, parentDiffEntry)) {
- continue;
- }
- FileHeader fileHeader = toFileHeader(parentCommitB, diffFormatter, parentDiffEntry);
- // The code which uses this PatchListEntry doesn't care about the last three parameters. As
- // they are expensive to compute, we use arbitrary values for them.
- PatchListEntry patchListEntry =
- newEntry(parentCommitA.getTree(), fileHeader, ImmutableSet.of(), 0, 0);
- parentPatchListEntries.add(patchListEntry);
- }
- return parentPatchListEntries;
- }
-
- private static Set<ContextAwareEdit> getEditsDueToRebase(
- Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath, DiffEntry diffEntry) {
- if (editsDueToRebasePerFilePath.isEmpty()) {
- return ImmutableSet.of();
- }
-
- String filePath = diffEntry.getNewPath();
- if (diffEntry.getChangeType() == ChangeType.DELETE) {
- filePath = diffEntry.getOldPath();
- }
- return ImmutableSet.copyOf(editsDueToRebasePerFilePath.get(filePath));
- }
-
- private Optional<PatchListEntry> getPatchListEntry(
- ObjectReader objectReader,
- DiffFormatter diffFormatter,
- DiffEntry diffEntry,
- RevTree treeA,
- RevTree treeB,
- Set<ContextAwareEdit> editsDueToRebase)
- throws IOException {
- FileHeader fileHeader = toFileHeader(key.getNewId(), diffFormatter, diffEntry);
- long oldSize =
- getFileSize(
- objectReader,
- diffEntry.getOldId(),
- diffEntry.getOldMode(),
- diffEntry.getOldPath(),
- treeA);
- long newSize =
- getFileSize(
- objectReader,
- diffEntry.getNewId(),
- diffEntry.getNewMode(),
- diffEntry.getNewPath(),
- treeB);
- Set<Edit> contentEditsDueToRebase = getContentEdits(editsDueToRebase);
- PatchListEntry patchListEntry =
- newEntry(treeA, fileHeader, contentEditsDueToRebase, newSize, newSize - oldSize);
- // All edits in a file are due to rebase -> exclude the file from the diff.
- if (EditTransformer.toEdits(toFileEdits(patchListEntry)).allMatch(editsDueToRebase::contains)) {
- return Optional.empty();
- }
- return Optional.of(patchListEntry);
- }
-
- private static Set<Edit> getContentEdits(Set<ContextAwareEdit> editsDueToRebase) {
- return editsDueToRebase.stream()
- .map(ContextAwareEdit::toEdit)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(toSet());
- }
-
- private ComparisonType getComparisonType(RevObject a, RevCommit b) {
- for (int i = 0; i < b.getParentCount(); i++) {
- if (b.getParent(i).equals(a)) {
- return ComparisonType.againstParent(i + 1);
- }
- }
-
- if (key.getOldId() == null && b.getParentCount() > 0) {
- return ComparisonType.againstAutoMerge();
- }
-
- return ComparisonType.againstOtherPatchSet();
- }
-
- private static long getFileSize(
- ObjectReader reader, AbbreviatedObjectId abbreviatedId, FileMode mode, String path, RevTree t)
- throws IOException {
- if (!isBlob(mode)) {
- return 0;
- }
- ObjectId fileId =
- toObjectId(reader, abbreviatedId).orElseGet(() -> lookupObjectId(reader, path, t));
- if (ObjectId.zeroId().equals(fileId)) {
- return 0;
- }
- return reader.getObjectSize(fileId, OBJ_BLOB);
- }
-
- private static boolean isBlob(FileMode mode) {
- int t = mode.getBits() & FileMode.TYPE_MASK;
- return t == FileMode.TYPE_FILE || t == FileMode.TYPE_SYMLINK;
- }
-
- private static Optional<ObjectId> toObjectId(
- ObjectReader reader, AbbreviatedObjectId abbreviatedId) throws IOException {
- if (abbreviatedId == null) {
- // In theory, DiffEntry#getOldId or DiffEntry#getNewId can be null for pure renames or pure
- // mode changes (e.g. DiffEntry#modify doesn't set the IDs). However, the method we call
- // for diffs (DiffFormatter#scan) seems to always produce DiffEntries with set IDs, even for
- // pure renames.
- return Optional.empty();
- }
- if (abbreviatedId.isComplete()) {
- // With the current JGit version and the method we call for diffs (DiffFormatter#scan), this
- // is the only code path taken right now.
- return Optional.ofNullable(abbreviatedId.toObjectId());
- }
- Collection<ObjectId> objectIds = reader.resolve(abbreviatedId);
- // It seems very unlikely that an ObjectId which was just abbreviated by the diff computation
- // now can't be resolved to exactly one ObjectId. The API allows this possibility, though.
- return objectIds.size() == 1
- ? Optional.of(Iterables.getOnlyElement(objectIds))
- : Optional.empty();
- }
-
- private static ObjectId lookupObjectId(ObjectReader reader, String path, RevTree tree) {
- // This variant is very expensive.
- try (TreeWalk treeWalk = TreeWalk.forPath(reader, path, tree)) {
- return treeWalk != null ? treeWalk.getObjectId(0) : ObjectId.zeroId();
- } catch (IOException e) {
- throw new StorageException(e);
- }
- }
-
- private FileHeader toFileHeader(
- ObjectId commitB, DiffFormatter diffFormatter, DiffEntry diffEntry) throws IOException {
-
- Future<FileHeader> result =
- diffExecutor.submit(
- () -> {
- synchronized (diffEntry) {
- return diffFormatter.toFileHeader(diffEntry);
- }
- });
-
- try {
- return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
- } catch (InterruptedException | TimeoutException e) {
- metrics.timeouts.increment();
- logger.atWarning().log(
- "%s ms timeout reached for Diff loader in project %s"
- + " on commit %s on path %s comparing %s..%s",
- timeoutMillis,
- project,
- commitB.name(),
- diffEntry.getNewPath(),
- diffEntry.getOldId().name(),
- diffEntry.getNewId().name());
- result.cancel(true);
- synchronized (diffEntry) {
- return toFileHeaderWithoutMyersDiff(diffFormatter, diffEntry);
- }
- } catch (ExecutionException e) {
- // If there was an error computing the result, carry it
- // up to the caller so the cache knows this key is invalid.
- Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
- throw new IOException(e.getMessage(), e.getCause());
- }
- }
-
- private FileHeader toFileHeaderWithoutMyersDiff(DiffFormatter diffFormatter, DiffEntry diffEntry)
- throws IOException {
- HistogramDiff histogramDiff = new HistogramDiff();
- histogramDiff.setFallbackAlgorithm(null);
- diffFormatter.setDiffAlgorithm(histogramDiff);
- return diffFormatter.toFileHeader(diffEntry);
- }
-
- private PatchListEntry newCommitMessage(
- RawTextComparator cmp, ObjectReader reader, RevCommit aCommit, RevCommit bCommit)
- throws IOException {
- Text aText = aCommit != null ? Text.forCommit(reader, aCommit) : Text.EMPTY;
- Text bText = Text.forCommit(reader, bCommit);
- return createPatchListEntry(cmp, aCommit, aText, bText, Patch.COMMIT_MSG);
- }
-
- private PatchListEntry newMergeList(
- RawTextComparator cmp,
- ObjectReader reader,
- RevCommit aCommit,
- RevCommit bCommit,
- ComparisonType comparisonType)
- throws IOException {
- Text aText = aCommit != null ? Text.forMergeList(comparisonType, reader, aCommit) : Text.EMPTY;
- Text bText = Text.forMergeList(comparisonType, reader, bCommit);
- return createPatchListEntry(cmp, aCommit, aText, bText, Patch.MERGE_LIST);
- }
-
- private static PatchListEntry createPatchListEntry(
- RawTextComparator cmp, RevCommit aCommit, Text aText, Text bText, String fileName) {
- byte[] rawHdr = getRawHeader(aCommit != null, fileName);
- byte[] aContent = aText.getContent();
- byte[] bContent = bText.getContent();
- long size = bContent.length;
- long sizeDelta = size - aContent.length;
- RawText aRawText = new RawText(aContent);
- RawText bRawText = new RawText(bContent);
- EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
- FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
- return new PatchListEntry(fh, edits, ImmutableSet.of(), size, sizeDelta);
- }
-
- private static byte[] getRawHeader(boolean hasA, String fileName) {
- StringBuilder hdr = new StringBuilder();
- hdr.append("diff --git");
- if (hasA) {
- hdr.append(" a/").append(fileName);
- } else {
- hdr.append(" ").append(FileHeader.DEV_NULL);
- }
- hdr.append(" b/").append(fileName);
- hdr.append("\n");
-
- if (hasA) {
- hdr.append("--- a/").append(fileName).append("\n");
- } else {
- hdr.append("--- ").append(FileHeader.DEV_NULL).append("\n");
- }
- hdr.append("+++ b/").append(fileName).append("\n");
- return hdr.toString().getBytes(UTF_8);
- }
-
- private static PatchListEntry newEntry(
- RevTree aTree, FileHeader fileHeader, Set<Edit> editsDueToRebase, long size, long sizeDelta) {
- if (aTree == null // want combined diff
- || fileHeader.getPatchType() != PatchType.UNIFIED
- || fileHeader.getHunks().isEmpty()) {
- return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta);
- }
-
- List<Edit> edits = fileHeader.toEditList();
- if (edits.isEmpty()) {
- return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta);
- }
- return new PatchListEntry(fileHeader, edits, editsDueToRebase, size, sizeDelta);
- }
-
- private RevObject aFor(
- PatchListKey key, Repository repo, RevWalk rw, InMemoryInserter ins, RevCommit b)
- throws IOException {
- if (key.getOldId() != null) {
- return rw.parseAny(key.getOldId());
- }
-
- switch (b.getParentCount()) {
- case 0:
- return rw.parseAny(emptyTree(ins));
- case 1:
- {
- RevCommit r = b.getParent(0);
- rw.parseBody(r);
- return r;
- }
- default:
- if (key.getParentNum() != null) {
- RevCommit r = b.getParent(key.getParentNum() - 1);
- rw.parseBody(r);
- return r;
- }
- // Only support auto-merge for 2 parents, not octopus merges
- if (b.getParentCount() == 2) {
- return autoMerger.lookupFromGitOrMergeInMemory(repo, rw, ins, b, mergeStrategy);
- }
- return null;
- }
- }
-
- private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
- ObjectId id = ins.insert(Constants.OBJ_TREE, new byte[] {});
- ins.flush();
- return id;
- }
-
- @AutoValue
- abstract static class EditsDueToRebaseResult {
- public static EditsDueToRebaseResult create(
- List<DiffEntry> relevantDiffEntries,
- Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath) {
- return new AutoValue_PatchListLoader_EditsDueToRebaseResult(
- relevantDiffEntries, editsDueToRebasePerFilePath);
- }
-
- public abstract List<DiffEntry> getRelevantOriginalDiffEntries();
-
- /** Returns the edits per file path they modify in {@code treeB}. */
- public abstract Multimap<String, ContextAwareEdit> getEditsDueToRebasePerFilePath();
- }
-}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index f028def..63a29cc 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -25,8 +25,12 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -38,7 +42,25 @@
private SubmitRequirementsAdapter() {}
- public static List<SubmitRequirementResult> createResult(
+ /**
+ * Retrieve legacy submit records (created by label functions and other {@link
+ * com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results.
+ */
+ public static Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
+ SubmitRuleEvaluator.Factory evaluator, ChangeData cd) {
+ // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
+ // This doesn't have an effect since we never call this class (i.e. to evaluate submit
+ // requirements) for closed changes.
+ List<SubmitRecord> records = evaluator.create(SubmitRuleOptions.defaults()).evaluate(cd);
+ List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
+ ObjectId commitId = cd.currentPatchSet().commitId();
+ return records.stream()
+ .map(r -> createResult(r, labelTypes, commitId))
+ .flatMap(List::stream)
+ .collect(Collectors.toMap(sr -> sr.submitRequirement(), Function.identity()));
+ }
+
+ static List<SubmitRequirementResult> createResult(
SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId) {
List<SubmitRequirementResult> results;
if (record.ruleName.equals("gerrit~DefaultSubmitRule")) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index b3ac380..402bb51 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -26,8 +26,13 @@
/**
* Evaluate and return all submit requirement results for a change. Submit requirements are read
* from the project config of the project containing the change as well as parent projects.
+ *
+ * @param cd change data corresponding to a specific gerrit change
+ * @param includeLegacy if set to true, evaluate legacy {@link
+ * com.google.gerrit.entities.SubmitRecord}s and convert them to submit requirements.
*/
- Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(ChangeData cd);
+ Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+ ChangeData cd, boolean includeLegacy);
/** Evaluate a single {@link SubmitRequirement} using change data. */
SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd);
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 9be50c7..de637b4 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -17,8 +17,6 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.entities.LabelType;
-import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
@@ -36,12 +34,8 @@
import com.google.inject.Provider;
import com.google.inject.Scopes;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import org.eclipse.jgit.lib.ObjectId;
/** Evaluates submit requirements for different change data. */
public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvaluator {
@@ -81,12 +75,19 @@
}
@Override
- public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(ChangeData cd) {
- Map<SubmitRequirement, SubmitRequirementResult> result = getRequirements(cd);
- if (experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
- result.putAll(getLegacyRequirements(cd));
+ public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+ ChangeData cd, boolean includeLegacy) {
+ Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements = getRequirements(cd);
+ Map<SubmitRequirement, SubmitRequirementResult> result = projectConfigRequirements;
+ if (includeLegacy
+ && experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
+ Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
+ SubmitRequirementsAdapter.getLegacyRequirements(legacyEvaluator, cd);
+ result =
+ SubmitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+ projectConfigRequirements, legacyReqs);
}
return ImmutableMap.copyOf(result);
}
@@ -140,23 +141,6 @@
return result;
}
- /**
- * Convert and return legacy submit records (created by label functions and other {@link
- * com.google.gerrit.server.rules.SubmitRule}s to submit requirement results.
- */
- private Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(ChangeData cd) {
- // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
- // This doesn't have an effect since we never call this class (i.e. to evaluate submit
- // requirements) for closed changes.
- List<SubmitRecord> records = legacyEvaluator.create(SubmitRuleOptions.defaults()).evaluate(cd);
- List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
- ObjectId commitId = cd.currentPatchSet().commitId();
- return records.stream()
- .map(r -> SubmitRequirementsAdapter.createResult(r, labelTypes, commitId))
- .flatMap(List::stream)
- .collect(Collectors.toMap(sr -> sr.submitRequirement(), Function.identity()));
- }
-
/** Evaluate the predicate recursively using change data. */
private PredicateResult evaluatePredicateTree(
Predicate<ChangeData> predicate, ChangeData changeData) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
new file mode 100644
index 0000000..2e43eac
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A utility class for different operations related to {@link
+ * com.google.gerrit.entities.SubmitRequirement}s.
+ */
+public class SubmitRequirementsUtil {
+
+ private SubmitRequirementsUtil() {}
+
+ /**
+ * Merge legacy and non-legacy submit requirement results. If both input maps have submit
+ * requirements with the same name and fulfillment status (according to {@link
+ * SubmitRequirementResult#fulfilled()}), we eliminate the entry from the {@code
+ * legacyRequirements} input map and only include the one from the {@code
+ * projectConfigRequirements} in the result.
+ *
+ * @param projectConfigRequirements map of {@link SubmitRequirement} to {@link
+ * SubmitRequirementResult} containing results for submit requirements stored in the
+ * project.config.
+ * @param legacyRequirements map of {@link SubmitRequirement} to {@link SubmitRequirementResult}
+ * containing the results of converting legacy submit records to submit requirements.
+ * @return a map that is the result of merging both input maps, while eliminating requirements
+ * with the same name and status.
+ */
+ public static Map<SubmitRequirement, SubmitRequirementResult> mergeLegacyAndNonLegacyRequirements(
+ Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
+ Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements) {
+ Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
+ result.putAll(projectConfigRequirements);
+ Map<String, SubmitRequirementResult> requirementsByName =
+ projectConfigRequirements.entrySet().stream()
+ .collect(Collectors.toMap(sr -> sr.getKey().name(), sr -> sr.getValue()));
+ for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy :
+ legacyRequirements.entrySet()) {
+ String name = legacy.getKey().name();
+ SubmitRequirementResult projectConfigResult = requirementsByName.get(name);
+ SubmitRequirementResult legacyResult = legacy.getValue();
+ if (projectConfigResult != null && matchByStatus(projectConfigResult, legacyResult)) {
+ continue;
+ }
+ result.put(legacy.getKey(), legacy.getValue());
+ }
+ return result;
+ }
+
+ /** Returns true if both input results are equal in allowing/disallowing change submission. */
+ private static boolean matchByStatus(SubmitRequirementResult r1, SubmitRequirementResult r2) {
+ return r1.fulfilled() == r2.fulfilled();
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index 55c27be..de7dd0a 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -58,13 +58,18 @@
Integer parentNum =
isInitialCommit(ctx.changeNotes().getProjectName(), targetPatchSet.commitId()) ? 0 : 1;
try {
- Map<String, FileDiffOutput> modifiedTargetPatchSet =
+ Map<String, FileDiffOutput> baseVsCurrent =
diffOperations.listModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(), targetPatchSet.commitId(), parentNum);
- Map<String, FileDiffOutput> modifiedSourcePatchSet =
+ Map<String, FileDiffOutput> baseVsPrior =
diffOperations.listModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(), sourcePatchSet.commitId(), parentNum);
- return match(modifiedTargetPatchSet, modifiedSourcePatchSet);
+ Map<String, FileDiffOutput> priorVsCurrent =
+ diffOperations.listModifiedFiles(
+ ctx.changeNotes().getProjectName(),
+ sourcePatchSet.commitId(),
+ targetPatchSet.commitId());
+ return match(baseVsCurrent, baseVsPrior, priorVsCurrent);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't copy"
@@ -79,16 +84,23 @@
* {@link ChangeType} matches for each modified file.
*/
public boolean match(
- Map<String, FileDiffOutput> modifiedFiles1, Map<String, FileDiffOutput> modifiedFiles2) {
+ Map<String, FileDiffOutput> baseVsCurrent,
+ Map<String, FileDiffOutput> baseVsPrior,
+ Map<String, FileDiffOutput> priorVsCurrent) {
Set<String> allFiles = new HashSet<>();
- allFiles.addAll(modifiedFiles1.keySet());
- allFiles.addAll(modifiedFiles2.keySet());
+ allFiles.addAll(baseVsCurrent.keySet());
+ allFiles.addAll(baseVsPrior.keySet());
for (String file : allFiles) {
if (Patch.isMagic(file)) {
continue;
}
- FileDiffOutput fileDiffOutput1 = modifiedFiles1.get(file);
- FileDiffOutput fileDiffOutput2 = modifiedFiles2.get(file);
+ FileDiffOutput fileDiffOutput1 = baseVsCurrent.get(file);
+ FileDiffOutput fileDiffOutput2 = baseVsPrior.get(file);
+ if (!priorVsCurrent.containsKey(file)) {
+ // If the file is not modified between prior and current patchsets, then scan safely skip
+ // it. The file might has been modified due to rebase.
+ continue;
+ }
if (fileDiffOutput1 == null || fileDiffOutput2 == null) {
return false;
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index c551cd2..b8c8c07 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -75,6 +75,8 @@
import com.google.gerrit.server.change.PureRevert;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -87,7 +89,9 @@
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRequirementsAdapter;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
+import com.google.gerrit.server.project.SubmitRequirementsUtil;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -268,7 +272,7 @@
ChangeData cd =
new ChangeData(
null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, project, id, null, null);
+ null, null, null, project, id, null, null);
cd.currentPatchSet =
PatchSet.builder()
.id(PatchSet.id(id, currentPatchSetId))
@@ -286,6 +290,7 @@
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory notesFactory;
private final CommentsUtil commentsUtil;
+ private final ExperimentFeatures experimentFeatures;
private final GitRepositoryManager repoManager;
private final MergeUtil.Factory mergeUtilFactory;
private final MergeabilityCache mergeabilityCache;
@@ -367,6 +372,7 @@
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory notesFactory,
CommentsUtil commentsUtil,
+ ExperimentFeatures experimentFeatures,
GitRepositoryManager repoManager,
MergeUtil.Factory mergeUtilFactory,
MergeabilityCache mergeabilityCache,
@@ -386,6 +392,7 @@
this.cmUtil = cmUtil;
this.notesFactory = notesFactory;
this.commentsUtil = commentsUtil;
+ this.experimentFeatures = experimentFeatures;
this.repoManager = repoManager;
this.mergeUtilFactory = mergeUtilFactory;
this.mergeabilityCache = mergeabilityCache;
@@ -946,13 +953,28 @@
return Collections.emptyMap();
}
Change c = change();
- if (c != null && c.isClosed()) {
+ if (c == null || !c.isClosed()) {
+ // Open changes: Evaluate submit requirements online.
submitRequirements =
- notes().getSubmitRequirementsResult().stream()
- .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
- } else {
- submitRequirements = submitRequirementsEvaluator.evaluateAllRequirements(this);
+ submitRequirementsEvaluator.evaluateAllRequirements(this, /* includeLegacy= */ true);
+ return submitRequirements;
}
+ // Closed changes: Load submit requirement results from NoteDb.
+ Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements =
+ notes().getSubmitRequirementsResult().stream()
+ .filter(r -> !r.legacy())
+ .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
+ if (!experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
+ submitRequirements = projectConfigRequirements;
+ return submitRequirements;
+ }
+ Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements =
+ SubmitRequirementsAdapter.getLegacyRequirements(submitRuleEvaluatorFactory, this);
+ submitRequirements =
+ SubmitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+ projectConfigRequirements, legacyRequirements);
}
return submitRequirements;
}
@@ -1347,7 +1369,14 @@
draftsByUser = new HashMap<>();
for (Ref ref : commentsUtil.getDraftRefs(notes().getChangeId())) {
Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null) {
+ if (account != null
+ // Double-check that any drafts exist for this user after
+ // filtering out zombies. If some but not all drafts in the ref
+ // were zombies, the returned Ref still includes those zombies;
+ // this is suboptimal, but is ok for the purposes of
+ // draftsByUser(), and easier than trying to rebuild the change at
+ // this point.
+ && !notes().getDraftComments(account, ref).isEmpty()) {
draftsByUser.put(account, ref.getObjectId());
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 59011f6..52202d7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -184,8 +184,6 @@
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.IntraLineDiff;
import com.google.gerrit.server.patch.IntraLineDiffKey;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListKey;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
@@ -243,10 +241,6 @@
@Inject private ExtensionRegistry extensionRegistry;
@Inject
- @Named("diff")
- private Cache<PatchListKey, PatchList> fileCache;
-
- @Inject
@Named("diff_intraline")
private Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
@@ -307,13 +301,10 @@
String fileContent = "First line\nSecond line\n";
PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
String triplet = project.get() + "~master~" + result.getChangeId();
- CacheStats startPatch = cloneStats(fileCache.stats());
CacheStats startIntra = cloneStats(intraCache.stats());
CacheStats startSummary = cloneStats(diffSummaryCache.stats());
gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
- assertThat(fileCache.stats()).since(startPatch).hasMissCount(0);
- assertThat(fileCache.stats()).since(startPatch).hasHitCount(0);
assertThat(intraCache.stats()).since(startIntra).hasMissCount(0);
assertThat(intraCache.stats()).since(startIntra).hasHitCount(0);
assertThat(diffSummaryCache.stats()).since(startSummary).hasMissCount(0);
@@ -4548,6 +4539,129 @@
value =
ExperimentFeaturesConstants
.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
+ public void
+ submitRequirements_returnOneEntryForMatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
+ throws Exception {
+ // Configure a legacy submit requirement: label with a max with block function
+ configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("build-cop-override")
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Configure a submit requirement with the same name.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("build-cop-override")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(
+ "label:build-cop-override=MAX -label:build-cop-override=MIN"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ // Create a change. Vote to fulfill all requirements.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ voteLabel(changeId, "build-cop-override", 1);
+ voteLabel(changeId, "Code-Review", 2);
+
+ // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco.
+ // Only non-legacy bco is returned.
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+
+ // Merge the change. Submit requirements are still the same.
+ gApi.changes().id(changeId).current().submit();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
+ public void
+ submitRequirements_returnTwoEntriesForMismatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
+ throws Exception {
+ // Configure a legacy submit requirement: label with a max with block function
+ configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("build-cop-override")
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Configure a submit requirement with the same name.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("build-cop-override")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:build-cop-override=MIN"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ // Create a change
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ voteLabel(changeId, "build-cop-override", 1);
+ voteLabel(changeId, "Code-Review", 2);
+
+ // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco.
+ // Two instances of bco will be returned since their status is not matching.
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(3);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.SATISFIED,
+ /* isLegacy= */ true,
+ // MAX_WITH_BLOCK function was translated to a submittability expression.
+ /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.UNSATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:build-cop-override=MIN");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
public void submitRequirements_returnForLegacySubmitRecords_ifEnabled() throws Exception {
configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
projectOperations
@@ -4591,6 +4705,7 @@
// 4. Merge the change. Submit requirements status is presented from NoteDb.
gApi.changes().id(changeId).current().submit();
change = gApi.changes().id(changeId).get();
+ // Legacy submit records are returned as submit requirements.
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
@@ -5205,6 +5320,30 @@
Collection<SubmitRequirementResultInfo> results,
String requirementName,
SubmitRequirementResultInfo.Status status,
+ boolean isLegacy,
+ String submittabilityCondition) {
+ for (SubmitRequirementResultInfo result : results) {
+ if (result.name.equals(requirementName)
+ && result.status == status
+ && result.isLegacy == isLegacy
+ && result.submittabilityExpressionResult.expression.equals(submittabilityCondition)) {
+ return;
+ }
+ }
+ throw new AssertionError(
+ String.format(
+ "Could not find submit requirement %s with status %s (results = %s)",
+ requirementName,
+ status,
+ results.stream()
+ .map(r -> String.format("%s=%s", r.name, r.status))
+ .collect(toImmutableList())));
+ }
+
+ private void assertSubmitRequirementStatus(
+ Collection<SubmitRequirementResultInfo> results,
+ String requirementName,
+ SubmitRequirementResultInfo.Status status,
boolean isLegacy) {
for (SubmitRequirementResultInfo result : results) {
if (result.name.equals(requirementName)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index cd9e876..3888679 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -43,6 +43,7 @@
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
@@ -557,6 +558,46 @@
}
@Test
+ public void
+ stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedDueToRebase_withoutCopyCondition()
+ throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .updateLabelType(
+ LabelId.CODE_REVIEW, b -> b.setCopyAllScoresIfListOfFilesDidNotChange(true));
+ u.save();
+ }
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Modify f.txt in change 1. Approve and submit the first change
+ gApi.changes().id(r.getChangeId()).edit().modifyFile("f.txt", RawInputUtil.create("content"));
+ gApi.changes().id(r.getChangeId()).edit().publish();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve().label(LabelId.VERIFIED, 1));
+ revision.submit();
+
+ // Add an approval whose score should be copied on change 2.
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the second change. The rebase adds f1.txt.
+ gApi.changes().id(r2.getChangeId()).rebase();
+
+ // The code-review approval is copied for the second change between PS1 and PS2 since the only
+ // modified file is due to rebase.
+ List<PatchSetApproval> patchSetApprovals =
+ r2.getChange().notes().getApprovalsWithCopied().values().stream()
+ .sorted(comparing(a -> a.patchSetId().get()))
+ .collect(toImmutableList());
+ PatchSetApproval nonCopied = patchSetApprovals.get(0);
+ PatchSetApproval copied = patchSetApprovals.get(1);
+ assertCopied(nonCopied, /* psId= */ 1, LabelId.CODE_REVIEW, (short) 1, false);
+ assertCopied(copied, /* psId= */ 2, LabelId.CODE_REVIEW, (short) 1, true);
+ }
+
+ @Test
public void stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModified_withCopyCondition()
throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
@@ -1072,17 +1113,9 @@
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
PatchSetApproval nonCopied = patchSetApprovals.get(0);
-
- assertThat(nonCopied.patchSetId().get()).isEqualTo(1);
- assertThat(nonCopied.label()).isEqualTo(LabelId.CODE_REVIEW);
- assertThat(nonCopied.value()).isEqualTo((short) 1);
- assertThat(nonCopied.copied()).isFalse();
-
PatchSetApproval copied = patchSetApprovals.get(1);
- assertThat(copied.patchSetId().get()).isEqualTo(2);
- assertThat(copied.label()).isEqualTo(LabelId.CODE_REVIEW);
- assertThat(copied.value()).isEqualTo((short) 1);
- assertThat(copied.copied()).isTrue();
+ assertCopied(nonCopied, 1, LabelId.CODE_REVIEW, (short) 1, /* copied= */ false);
+ assertCopied(copied, 2, LabelId.CODE_REVIEW, (short) 1, /* copied= */ true);
}
@Test
@@ -1311,4 +1344,12 @@
}
assertWithMessage(name).that(vote).isEqualTo(expectedVote);
}
+
+ private void assertCopied(
+ PatchSetApproval approval, int psId, String label, short value, boolean copied) {
+ assertThat(approval.patchSetId().get()).isEqualTo(psId);
+ assertThat(approval.label()).isEqualTo(label);
+ assertThat(approval.value()).isEqualTo(value);
+ assertThat(approval.copied()).isEqualTo(copied);
+ }
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 4e7b3f3..c524c94 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -76,6 +76,8 @@
import org.junit.Test;
public class ChangeNotesTest extends AbstractChangeNotesTest {
+ @Inject private DraftCommentNotes.Factory draftNotesFactory;
+
@Inject private ChangeNoteJson changeNoteJson;
@Test
@@ -2978,6 +2980,86 @@
}
@Test
+ public void filterOutAndFixUpZombieDraftComments() throws Exception {
+ Change c = newChange();
+ ObjectId commitId1 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+ CommentRange range = new CommentRange(1, 1, 2, 1);
+ PatchSet.Id ps1 = c.currentPatchSetId();
+ short side = (short) 1;
+
+ ChangeUpdate update = newUpdate(c, otherUser);
+ Timestamp now = TimeUtil.nowTs();
+ HumanComment comment1 =
+ newComment(
+ ps1,
+ "file1",
+ "uuid1",
+ range,
+ range.getEndLine(),
+ otherUser,
+ null,
+ now,
+ "comment on ps1",
+ side,
+ commitId1,
+ false);
+ HumanComment comment2 =
+ newComment(
+ ps1,
+ "file2",
+ "uuid2",
+ range,
+ range.getEndLine(),
+ otherUser,
+ null,
+ now,
+ "another comment",
+ side,
+ commitId1,
+ false);
+ update.putComment(HumanComment.Status.DRAFT, comment1);
+ update.putComment(HumanComment.Status.DRAFT, comment2);
+ update.commit();
+
+ String refName = refsDraftComments(c.getId(), otherUserId);
+ ObjectId oldDraftId = exactRefAllUsers(refName);
+
+ update = newUpdate(c, otherUser);
+ update.setPatchSetId(ps1);
+ update.putComment(HumanComment.Status.PUBLISHED, comment2);
+ update.commit();
+ assertThat(exactRefAllUsers(refName)).isNotNull();
+ assertThat(exactRefAllUsers(refName)).isNotEqualTo(oldDraftId);
+
+ // Re-add draft version of comment2 back to draft ref without updating
+ // change ref. Simulates the case where deleting the draft failed
+ // non-atomically after adding the published comment succeeded.
+ ChangeDraftUpdate draftUpdate = newUpdate(c, otherUser).createDraftUpdateIfNull();
+ draftUpdate.putComment(comment2);
+ try (NoteDbUpdateManager manager = updateManagerFactory.create(c.getProject())) {
+ manager.add(draftUpdate);
+ manager.execute();
+ }
+
+ // Looking at drafts directly shows the zombie comment.
+ DraftCommentNotes draftNotes = draftNotesFactory.create(c.getId(), otherUserId);
+ assertThat(draftNotes.load().getComments().get(commitId1)).containsExactly(comment1, comment2);
+
+ // Zombie comment is filtered out of drafts via ChangeNotes.
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getDraftComments(otherUserId).get(commitId1)).containsExactly(comment1);
+ assertThat(notes.getHumanComments().get(commitId1)).containsExactly(comment2);
+
+ update = newUpdate(c, otherUser);
+ update.setPatchSetId(ps1);
+ update.putComment(HumanComment.Status.PUBLISHED, comment1);
+ update.commit();
+
+ // Updating an unrelated comment causes the zombie comment to get fixed up.
+ assertThat(exactRefAllUsers(refName)).isNull();
+ }
+
+ @Test
public void updateCommentsInSequentialUpdates() throws Exception {
Change c = newChange();
CommentRange range = new CommentRange(1, 1, 2, 1);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 19c2bcf..7f16cc4 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -785,7 +785,7 @@
.containsExactly(
"@@ -6 +6 @@\n"
+ "-Removed Verified+2 by Other Account <other@account.com>\n"
- + "+Removed Verified+2 by Gerrit Account\n");
+ + "+Removed Verified+2\n");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -1671,10 +1671,9 @@
+ " * file1.java\n"
+ "\n<GERRIT_ACCOUNT_2>, who was added as reviewer owns the following files:\n"
+ " * file3.js\n"
- + "\nGerrit Account, who was added as reviewer owns the following files:\n"
+ + "\nAdded reviewer owns the following files:\n"
+ " * file4.java\n",
- "Gerrit Account, who was added as reviewer owns the following files:\n"
- + " * file6.java\n",
+ "Added reviewer owns the following files:\n" + " * file6.java\n",
"Gerrit Account who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
+ "\n<GERRIT_ACCOUNT_1> who was added as reviewer owns the following files:\n"
@@ -1701,10 +1700,10 @@
+ "+<GERRIT_ACCOUNT_2>, who was added as reviewer owns the following files:\n"
+ "@@ -12 +12 @@\n"
+ "-Missing Reviewer who was added as reviewer owns the following files:\n"
- + "+Gerrit Account, who was added as reviewer owns the following files:\n",
+ + "+Added reviewer owns the following files:\n",
"@@ -6 +6 @@\n"
+ "-Reviewer User who was added as reviewer owns the following files:\n"
- + "+Gerrit Account, who was added as reviewer owns the following files:\n");
+ + "+Added reviewer owns the following files:\n");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -2051,7 +2050,8 @@
getChangeUpdateBody(
c,
String.format(
- "Assignee changed from: %s to: %s", changeOwner.getName(), otherUser.getName())),
+ "Assignee changed from: %s to: %s",
+ changeOwner.getNameEmail(), otherUser.getNameEmail())),
getAuthorIdent(otherUser.getAccount()));
writeUpdate(
RefNames.changeMetaRef(c.getId()),
@@ -2086,14 +2086,12 @@
+ "-Assignee added: Change Owner\n"
+ "+Assignee added: <GERRIT_ACCOUNT_1>\n",
"@@ -6 +6 @@\n"
- + "-Assignee changed from: Change Owner to: Other Account\n"
+ + "-Assignee changed from: Change Owner <change@owner.com> to: Other Account <other@account.com>\n"
+ "+Assignee changed from: <GERRIT_ACCOUNT_1> to: <GERRIT_ACCOUNT_2>\n",
"@@ -6 +6 @@\n"
+ "-Assignee deleted: Other Account\n"
+ "+Assignee deleted: <GERRIT_ACCOUNT_2>\n",
- "@@ -6 +6 @@\n"
- + "-Assignee added: Reviewer User\n"
- + "+Assignee added: Gerrit Account\n");
+ "@@ -6 +6 @@\n" + "-Assignee added: Reviewer User\n" + "+Assignee was added.\n");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
assertThat(secondRunResult.refsFailedToFix).isEmpty();
diff --git a/javatests/com/google/gerrit/server/patch/PatchListTest.java b/javatests/com/google/gerrit/server/patch/PatchListTest.java
deleted file mode 100644
index 182ce49..0000000
--- a/javatests/com/google/gerrit/server/patch/PatchListTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.patch;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.entities.Patch.ChangeType;
-import com.google.gerrit.server.patch.PatchList.ChangeTypeCmp;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Test;
-
-public class PatchListTest {
-
- @Test
- public void fileOrder() {
- String[] names = {
- "zzz",
- "def/g",
- "/!xxx",
- "abc",
- Patch.MERGE_LIST,
- "qrx",
- Patch.COMMIT_MSG,
- Patch.PATCHSET_LEVEL
- };
- String[] want = {
- Patch.COMMIT_MSG,
- Patch.MERGE_LIST,
- Patch.PATCHSET_LEVEL,
- "/!xxx",
- "abc",
- "def/g",
- "qrx",
- "zzz",
- };
- Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
- assertThat(names).isEqualTo(want);
- }
-
- @Test
- public void fileOrderNoMerge() {
- String[] names = {
- "zzz", "def/g", "/!xxx", "abc", "qrx", Patch.COMMIT_MSG,
- };
- String[] want = {
- Patch.COMMIT_MSG, "/!xxx", "abc", "def/g", "qrx", "zzz",
- };
-
- Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
- assertThat(names).isEqualTo(want);
- }
-
- @Test
- public void changeTypeOrderIsComplete() {
- List<ChangeType> changeTypeOrder = ChangeTypeCmp.order;
- ChangeType[] allTypes = ChangeType.values();
-
- Arrays.sort(allTypes, PatchList.CHANGE_TYPE_CMP);
- assertThat(changeTypeOrder).containsExactlyElementsIn(allTypes).inOrder();
- }
-
- @Test
- public void largeObjectTombstoneCanBeSerializedAndDeserialized() throws Exception {
- // Serialize
- byte[] serializedObject;
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream objectStream = new ObjectOutputStream(baos)) {
- objectStream.writeObject(new PatchListCacheImpl.LargeObjectTombstone());
- serializedObject = baos.toByteArray();
- assertThat(serializedObject).isNotNull();
- }
- // Deserialize
- try (InputStream is = new ByteArrayInputStream(serializedObject);
- ObjectInputStream ois = new ObjectInputStream(is)) {
- assertThat(ois.readObject()).isInstanceOf(PatchListCacheImpl.LargeObjectTombstone.class);
- }
- }
-}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 2663853..9ebee9c 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -69,6 +69,7 @@
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -2333,6 +2334,44 @@
}
@Test
+ public void byHasDraftExcludesZombieDrafts() throws Exception {
+ Project.NameKey project = Project.nameKey("repo");
+ TestRepository<Repo> repo = createProject(project.get());
+ Change change = insert(repo, newChange(repo));
+ Change.Id id = change.getId();
+
+ DraftInput in = new DraftInput();
+ in.line = 1;
+ in.message = "nit: trailing whitespace";
+ in.path = Patch.COMMIT_MSG;
+ gApi.changes().id(id.get()).current().createDraft(in);
+
+ assertQuery("has:draft", change);
+ assertQuery("commentby:" + userId);
+
+ try (TestRepository<Repo> allUsers =
+ new TestRepository<>(repoManager.openRepository(allUsersName))) {
+ Ref draftsRef = allUsers.getRepository().exactRef(RefNames.refsDraftComments(id, userId));
+ assertThat(draftsRef).isNotNull();
+
+ ReviewInput rin = ReviewInput.dislike();
+ rin.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ gApi.changes().id(id.get()).current().review(rin);
+
+ assertQuery("has:draft");
+ assertQuery("commentby:" + userId, change);
+ assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNull();
+
+ // Re-add drafts ref and ensure it gets filtered out during indexing.
+ allUsers.update(draftsRef.getName(), draftsRef.getObjectId());
+ assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNotNull();
+ }
+
+ indexer.index(project, id);
+ assertQuery("has:draft");
+ }
+
+ @Test
public void byStarredBy() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index ba62ec3..8b46cd8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -752,6 +752,16 @@
})
);
}
+
+ _showNewSubmitRequirements(change?: ParsedChangeInfo) {
+ if (!this._isSubmitRequirementsUiEnabled) return false;
+ return (change?.submit_requirements ?? []).length > 0;
+ }
+
+ _showNewSubmitRequirementWarning(change?: ParsedChangeInfo) {
+ if (!this._isSubmitRequirementsUiEnabled) return false;
+ return (change?.submit_requirements ?? []).length === 0;
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index 26d1277..97101f6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -113,6 +113,10 @@
--iron-icon-height: 18px;
--iron-icon-width: 18px;
}
+ .submit-requirement-error {
+ color: var(--deemphasized-text-color);
+ padding-left: var(--metadata-horizontal-padding);
+ }
</style>
<gr-external-style id="externalStyle" name="change-metadata">
<div class="metadata-header">
@@ -480,20 +484,25 @@
</span>
</section>
<div class="separatedSection">
- <template is="dom-if" if="[[_isSubmitRequirementsUiEnabled]]">
+ <template is="dom-if" if="[[_showNewSubmitRequirements(change)]]">
<gr-submit-requirements
change="[[change]]"
account="[[account]]"
mutable="[[_mutable]]"
></gr-submit-requirements>
</template>
- <template is="dom-if" if="[[!_isSubmitRequirementsUiEnabled]]">
+ <template is="dom-if" if="[[!_showNewSubmitRequirements(change)]]">
<gr-change-requirements
change="{{change}}"
account="[[account]]"
mutable="[[_mutable]]"
></gr-change-requirements>
</template>
+ <template is="dom-if" if="[[_showNewSubmitRequirementWarning(change)]]">
+ <div class="submit-requirement-error">
+ New Submit Requirements don't work on this change.
+ </div>
+ </template>
</div>
<section
id="webLinks"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index fd7b5d1..fc2cbe5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1824,7 +1824,7 @@
changeIsOpen(change)
) {
fireAlert(this, 'Change edit not found. Please create a change edit.');
- GerritNav.navigateToChange(change);
+ fireReload(this, true);
return;
}
@@ -1837,7 +1837,7 @@
this,
'Change edits cannot be created if change is merged or abandoned. Redirected to non edit mode.'
);
- GerritNav.navigateToChange(change);
+ fireReload(this, true);
return;
}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 2e00034..4b1dba6 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -15,7 +15,8 @@
* limitations under the License.
*/
import '../../../styles/gr-font-styles';
-import '../../shared/gr-hovercard/gr-hovercard-shared-style';
+import '../../../styles/gr-hovercard-styles';
+import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {customElement, property} from '@polymer/decorators';
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
index b7b4d9c..5023895 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
@@ -20,7 +20,10 @@
<style include="gr-font-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <style include="gr-hovercard-shared-style">
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-hovercard-styles">
#container {
min-width: 356px;
max-width: 356px;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 48ccf2c..9c27cdb 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -293,7 +293,13 @@
override firstUpdated() {
const loading = this.shadowRoot?.querySelector('.container');
assertIsDefined(loading, '"Loading" element');
- whenVisible(loading, () => this.setAttribute('shouldRender', 'true'), 200);
+ whenVisible(
+ loading,
+ () => {
+ this.shouldRender = true;
+ },
+ 200
+ );
}
override render() {
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index d26856c..57eac3b 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -16,6 +16,8 @@
*/
import './gr-checks-styles';
import '../../styles/gr-font-styles';
+import '../../styles/gr-hovercard-styles';
+import '../../styles/shared-styles';
import {HovercardBehaviorMixin} from '../shared/gr-hovercard/gr-hovercard-behavior';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-hovercard-run_html';
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
index 49a1416..52dbb9c 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
@@ -23,7 +23,10 @@
<style include="gr-checks-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <style include="gr-hovercard-shared-style">
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-hovercard-styles">
#container {
min-width: 356px;
max-width: 356px;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 78c1ebd..aaeb924 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -55,7 +55,6 @@
'commentby:',
'commit:',
'committer:',
- 'conflicts:',
'deleted:',
'delta:',
'dir:',
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 4641897..b1bad1c 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
@@ -56,6 +56,7 @@
PatchRange,
PatchSetNum,
RepoName,
+ UrlEncodedCommentId,
} from '../../../types/common';
import {
DiffInfo,
@@ -730,11 +731,24 @@
}
_threadsChanged(threads: CommentThread[]) {
- const threadEls = new Set<Object>();
+ const threadEls = new Set<GrCommentThread>();
+ const rootIdToThreadEl = new Map<UrlEncodedCommentId, GrCommentThread>();
+ for (const threadEl of this.getThreadEls()) {
+ if (threadEl.rootId) {
+ rootIdToThreadEl.set(threadEl.rootId, threadEl);
+ }
+ }
for (const thread of threads) {
- const threadEl = this._createThreadElement(thread);
- this._attachThreadElement(threadEl);
- threadEls.add(threadEl);
+ const existingThreadEl =
+ thread.rootId && rootIdToThreadEl.get(thread.rootId);
+ if (existingThreadEl) {
+ this._updateThreadElement(existingThreadEl, thread);
+ threadEls.add(existingThreadEl);
+ } else {
+ const threadEl = this._createThreadElement(thread);
+ this._attachThreadElement(threadEl);
+ threadEls.add(threadEl);
+ }
}
// Remove all threads that are no longer existing.
for (const threadEl of this.getThreadEls()) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index ed3ffe0..8901636 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -1145,11 +1145,19 @@
.queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 1);
-
element.threads= [...element.threads, thread];
threads = dom(element.$.diff)
.queryDistributedElements('gr-comment-thread');
+ // Threads have same rootId so element is reused
+ assert.equal(threads.length, 1);
+
+ const newThread = {...thread};
+ newThread.rootId = 'differentRootId';
+ element.threads= [...element.threads, newThread];
+ threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+ // New thread has a different rootId
assert.equal(threads.length, 2);
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 3a05e7d..32f5f39 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -422,25 +422,23 @@
/>
`;
- const sourceImageWithHighlight = this.diffHighlightSrc
- ? html`
- <div id="source-plus-highlight-container">
- ${sourceImage}
- <img
- id="highlight-image"
- style="${styleMap({
- opacity: this.showHighlight ? '1' : '0',
- // When the highlight layer is not being shown, saving the image or
- // opening it in a new tab from the context menu, e.g. for external
- // comparison, should give back the source image, not the highlight
- // layer.
- 'pointer-events': this.showHighlight ? 'auto' : 'none',
- })}"
- src="${this.diffHighlightSrc}"
- />
- </div>
- `
- : '';
+ const sourceImageWithHighlight = html`
+ <div id="source-plus-highlight-container">
+ ${sourceImage}
+ <img
+ id="highlight-image"
+ style="${styleMap({
+ opacity: this.showHighlight ? '1' : '0',
+ // When the highlight layer is not being shown, saving the image or
+ // opening it in a new tab from the context menu, e.g. for external
+ // comparison, should give back the source image, not the highlight
+ // layer.
+ 'pointer-events': this.showHighlight ? 'auto' : 'none',
+ })}"
+ src="${this.diffHighlightSrc}"
+ />
+ </div>
+ `;
const versionExplanation = html`
<div id="version-explanation">
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index d87b573..1615a23 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -36,7 +36,7 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {appContext} from '../../../services/app-context';
import {IronInputElement} from '@polymer/iron-input';
-import {fireAlert} from '../../../utils/event-util';
+import {fireAlert, fireReload} from '../../../utils/event-util';
export interface GrEditControls {
$: {
@@ -237,7 +237,7 @@
return;
}
this._closeDialog(this.$.openDialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this, true);
});
}
@@ -257,7 +257,7 @@
return;
}
this._closeDialog(dialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this);
});
}
@@ -275,7 +275,7 @@
return;
}
this._closeDialog(dialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this);
});
}
@@ -293,7 +293,7 @@
return;
}
this._closeDialog(dialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this, true);
});
}
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 0ba68e2..6198f17 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -122,12 +122,12 @@
});
suite('delete button CUJ', () => {
- let navStub: sinon.SinonStub;
+ let eventStub: sinon.SinonStub;
let deleteStub: sinon.SinonStub;
let deleteAutocomplete: GrAutocomplete;
setup(() => {
- navStub = sinon.stub(GerritNav, 'navigateToChange');
+ eventStub = sinon.stub(element, 'dispatchEvent');
deleteStub = stubRestApi('deleteFileInChangeEdit');
deleteAutocomplete =
element.$.deleteDialog!.querySelector('gr-autocomplete')!;
@@ -155,7 +155,7 @@
assert.isTrue(deleteStub.called);
await deleteStub.lastCall.returnValue;
assert.equal(element._path, '');
- assert.isTrue(navStub.called);
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
assert.isTrue(closeDialogSpy.called);
});
@@ -181,7 +181,7 @@
assert.isTrue(deleteStub.called);
await deleteStub.lastCall.returnValue;
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isFalse(closeDialogSpy.called);
});
@@ -195,7 +195,7 @@
MockInteractions.tap(
queryAndAssert(element.$.deleteDialog, 'gr-button')
);
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, '');
});
@@ -203,12 +203,12 @@
});
suite('rename button CUJ', () => {
- let navStub: sinon.SinonStub;
+ let eventStub: sinon.SinonStub;
let renameStub: sinon.SinonStub;
let renameAutocomplete: GrAutocomplete;
setup(() => {
- navStub = sinon.stub(GerritNav, 'navigateToChange');
+ eventStub = sinon.stub(element, 'dispatchEvent');
renameStub = stubRestApi('renameFileInChangeEdit');
renameAutocomplete =
element.$.renameDialog!.querySelector('gr-autocomplete')!;
@@ -241,7 +241,7 @@
await renameStub.lastCall.returnValue;
assert.equal(element._path, '');
- assert.isTrue(navStub.called);
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
assert.isTrue(closeDialogSpy.called);
});
@@ -272,7 +272,7 @@
assert.isTrue(renameStub.called);
await renameStub.lastCall.returnValue;
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isFalse(closeDialogSpy.called);
});
@@ -287,7 +287,7 @@
MockInteractions.tap(
queryAndAssert(element.$.renameDialog, 'gr-button')
);
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, '');
assert.equal(element._newPath, '');
@@ -296,11 +296,11 @@
});
suite('restore button CUJ', () => {
- let navStub: sinon.SinonStub;
+ let eventStub: sinon.SinonStub;
let restoreStub: sinon.SinonStub;
setup(() => {
- navStub = sinon.stub(GerritNav, 'navigateToChange');
+ eventStub = sinon.stub(element, 'dispatchEvent');
restoreStub = stubRestApi('restoreFileInChangeEdit');
});
@@ -324,7 +324,7 @@
assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
return restoreStub.lastCall.returnValue.then(() => {
assert.equal(element._path, '');
- assert.isTrue(navStub.called);
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
assert.isTrue(closeDialogSpy.called);
});
});
@@ -343,7 +343,7 @@
assert.isTrue(restoreStub.called);
assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
return restoreStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isFalse(closeDialogSpy.called);
});
});
@@ -356,7 +356,7 @@
MockInteractions.tap(
queryAndAssert(element.$.restoreDialog, 'gr-button')
);
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, '');
});
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 3b93bea..38f55bd 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -242,7 +242,7 @@
this.addEventListener(EventType.DIALOG_CHANGE, e => {
this._handleDialogChange(e as CustomEvent<DialogChangeEventDetail>);
});
- this.addEventListener('location-change', e =>
+ this.addEventListener(EventType.LOCATION_CHANGE, e =>
this._handleLocationChange(e)
);
this.addEventListener(EventType.RECREATE_CHANGE_VIEW, () =>
@@ -251,7 +251,7 @@
this.addEventListener(EventType.RECREATE_DIFF_VIEW, () =>
this.handleRecreateView(GerritView.DIFF)
);
- document.addEventListener('gr-rpc-log', e => this._handleRpcLog(e));
+ document.addEventListener(EventType.GR_RPC_LOG, e => this._handleRpcLog(e));
}
override ready() {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 06272ed..3988095 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -18,6 +18,7 @@
import '@polymer/iron-icon/iron-icon';
import '../../../styles/gr-font-styles';
import '../../../styles/shared-styles';
+import '../../../styles/gr-hovercard-styles';
import '../gr-avatar/gr-avatar';
import '../gr-button/gr-button';
import {HovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior';
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
index adca888..cba7293 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
@@ -14,14 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../gr-hovercard/gr-hovercard-shared-style';
+import '../../../styles/gr-hovercard-styles';
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
<style include="gr-font-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <style include="gr-hovercard-shared-style">
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-hovercard-styles">
.top,
.attention,
.status,
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
index 82af365..3d8702b 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
@@ -129,12 +129,8 @@
super.connectedCallback();
if (!this._target) {
this._target = this.target;
+ this.addTargetEventListeners();
}
- this._target.addEventListener('mouseenter', this.debounceShow);
- this._target.addEventListener('focus', this.debounceShow);
- this._target.addEventListener('mouseleave', this.debounceHide);
- this._target.addEventListener('blur', this.debounceHide);
- this._target.addEventListener('click', this.hide);
// show the hovercard if mouse moves to hovercard
// this will cancel pending hide as well
@@ -149,12 +145,23 @@
this.cancelShowTask();
this.cancelHideTask();
this.unlock();
+ super.disconnectedCallback();
+ }
+
+ addTargetEventListeners() {
+ this._target?.addEventListener('mouseenter', this.debounceShow);
+ this._target?.addEventListener('focus', this.debounceShow);
+ this._target?.addEventListener('mouseleave', this.debounceHide);
+ this._target?.addEventListener('blur', this.debounceHide);
+ this._target?.addEventListener('click', this.hide);
+ }
+
+ removeTargetEventListeners() {
this._target?.removeEventListener('mouseenter', this.debounceShow);
this._target?.removeEventListener('focus', this.debounceShow);
this._target?.removeEventListener('mouseleave', this.debounceHide);
this._target?.removeEventListener('blur', this.debounceHide);
this._target?.removeEventListener('click', this.hide);
- super.disconnectedCallback();
}
override ready() {
@@ -457,7 +464,9 @@
*/
@observe('for')
_forChanged() {
+ this.removeTargetEventListeners();
this._target = this.target;
+ this.addTargetEventListeners();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
deleted file mode 100644
index aa92654..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Mark the file as a module. Otherwise typescript assumes this is a script
-// and $_documentContainer is a global variable.
-// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
-
-/** The shared styles for all hover cards. */
-const GrHoverCardSharedStyle = document.createElement('dom-module');
-GrHoverCardSharedStyle.innerHTML = `<template>
- <style include="shared-styles">
- :host {
- position: absolute;
- display: none;
- z-index: 200;
- max-width: 600px;
- outline: none;
- }
- :host(.hovered) {
- display: block;
- }
- :host(.hide) {
- visibility: hidden;
- }
- /* You have to use a <div class="container"> in your hovercard in order
- to pick up this consistent styling. */
- #container {
- background: var(--dialog-background-color);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- box-shadow: var(--elevation-level-5);
- }
- </style>
- </template>`;
-
-GrHoverCardSharedStyle.register('gr-hovercard-shared-style');
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
index acc5e15..5fc53e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
@@ -15,20 +15,35 @@
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-hovercard_html';
-import {HovercardBehaviorMixin} from './gr-hovercard-behavior';
-import './gr-hovercard-shared-style';
-import {customElement} from '@polymer/decorators';
+import {customElement} from 'lit/decorators';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {css, html, LitElement} from 'lit';
+import {hovercardStyles} from '../../../styles/gr-hovercard-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
@customElement('gr-hovercard')
export class GrHovercard extends base {
- static get template() {
- return htmlTemplate;
+ static override get styles() {
+ return [
+ sharedStyles,
+ hovercardStyles,
+ css`
+ #container {
+ padding: var(--spacing-l);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div id="container" role="tooltip" tabindex="-1">
+ <slot></slot>
+ </div>
+ `;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
deleted file mode 100644
index 830cbd878..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-hovercard-shared-style">
- #container {
- padding: var(--spacing-l);
- }
- </style>
- <div id="container" role="tooltip" tabindex="-1">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
deleted file mode 100644
index d5e0061..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-hovercard.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-const basicFixture = fixtureFromTemplate(html`
-<gr-hovercard for="foo" id="bar"></gr-hovercard>
-`);
-
-suite('gr-hovercard tests', () => {
- let element;
-
- let button;
- let testResolve;
- let testPromise;
-
- setup(() => {
- testResolve = undefined;
- testPromise = new Promise(r => testResolve = r);
- button = document.createElement('button');
- button.innerHTML = 'Hello';
- button.setAttribute('id', 'foo');
- document.body.appendChild(button);
-
- element = basicFixture.instantiate();
- });
-
- teardown(() => {
- element.hide({});
- button.remove();
- });
-
- test('updatePosition', () => {
- // Test that the correct style properties have at least been set.
- element.position = 'bottom';
- element.updatePosition();
- assert.typeOf(element.style.getPropertyValue('left'), 'string');
- assert.typeOf(element.style.getPropertyValue('top'), 'string');
- assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
- assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
-
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = element._target.getBoundingClientRect();
- const thisRect = element.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- const pixelCompare = pixel =>
- Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
-
- assert.equal(
- pixelCompare(element.style.left),
- pixelCompare(
- (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
- assert.equal(
- pixelCompare(element.style.top),
- pixelCompare(
- (targetTop + targetRect.height + element.offset) + 'px'));
- });
-
- test('hide', () => {
- element.hide({});
- const style = getComputedStyle(element);
- assert.isFalse(element._isShowing);
- assert.isFalse(element.classList.contains('hovered'));
- assert.equal(style.display, 'none');
- assert.notEqual(element.container, element.parentNode);
- });
-
- test('show', async () => {
- await element.show({});
- const style = getComputedStyle(element);
- assert.isTrue(element._isShowing);
- assert.isTrue(element.classList.contains('hovered'));
- assert.equal(style.opacity, '1');
- assert.equal(style.visibility, 'visible');
- });
-
- test('debounceShow does not show immediately', async () => {
- element.debounceShowBy(100);
- setTimeout(testResolve, 0);
- await testPromise;
- assert.isFalse(element._isShowing);
- });
-
- test('debounceShow shows after delay', async () => {
- element.debounceShowBy(1);
- setTimeout(testResolve, 10);
- await testPromise;
- assert.isTrue(element._isShowing);
- });
-
- test('card is scheduled to show on enter and hides on leave', async () => {
- const button = document.querySelector('button');
- let enterResolve = undefined;
- const enterPromise = new Promise(r => enterResolve = r);
- button.addEventListener('mouseenter', enterResolve);
- let leaveResolve = undefined;
- const leavePromise = new Promise(r => leaveResolve = r);
- button.addEventListener('mouseleave', leaveResolve);
-
- assert.isFalse(element._isShowing);
- button.dispatchEvent(new CustomEvent('mouseenter'));
-
- await enterPromise;
- await flush();
- assert.isTrue(element.isScheduledToShow);
- element.showTask.flush();
- assert.isTrue(element._isShowing);
- assert.isFalse(element.isScheduledToShow);
-
- button.dispatchEvent(new CustomEvent('mouseleave'));
-
- await leavePromise;
- assert.isTrue(element.isScheduledToHide);
- assert.isTrue(element._isShowing);
- element.hideTask.flush();
- assert.isFalse(element.isScheduledToShow);
- assert.isFalse(element._isShowing);
-
- button.removeEventListener('mouseenter', enterResolve);
- button.removeEventListener('mouseleave', leaveResolve);
- });
-
- test('card should disappear on click', async () => {
- const button = document.querySelector('button');
- let enterResolve = undefined;
- const enterPromise = new Promise(r => enterResolve = r);
- button.addEventListener('mouseenter', enterResolve);
- let clickResolve = undefined;
- const clickPromise = new Promise(r => clickResolve = r);
- button.addEventListener('click', clickResolve);
-
- assert.isFalse(element._isShowing);
-
- button.dispatchEvent(new CustomEvent('mouseenter'));
-
- await enterPromise;
- await flush();
- assert.isTrue(element.isScheduledToShow);
- MockInteractions.tap(button);
-
- await clickPromise;
- assert.isFalse(element.isScheduledToShow);
- assert.isFalse(element._isShowing);
-
- button.removeEventListener('mouseenter', enterResolve);
- button.removeEventListener('click', clickResolve);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index 5a6d821..2df2ccb 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -24,7 +24,6 @@
import '../gr-label/gr-label';
import '../gr-tooltip-content/gr-tooltip-content';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {
AccountInfo,
LabelInfo,
@@ -44,6 +43,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {votingStyles} from '../../../styles/gr-voting-styles';
import {ifDefined} from 'lit/directives/if-defined';
+import {fireReload} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -349,7 +349,7 @@
return;
}
if (this.change) {
- GerritNav.navigateToChange(this.change);
+ fireReload(this);
}
})
.catch(err => {
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
new file mode 100644
index 0000000..793e5d6
--- /dev/null
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -0,0 +1,488 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {getRootElement} from '../../scripts/rootElement';
+import {Constructor} from '../../utils/common-util';
+import {LitElement, PropertyValues} from 'lit';
+import {property, query} from 'lit/decorators';
+import {ShowAlertEventDetail} from '../../types/events';
+import {debounce, DelayedTask} from '../../utils/async-util';
+import {hovercardStyles} from '../../styles/gr-hovercard-styles';
+import {sharedStyles} from '../../styles/shared-styles';
+
+interface ReloadEventDetail {
+ clearPatchset?: boolean;
+}
+
+const HOVER_CLASS = 'hovered';
+const HIDE_CLASS = 'hide';
+
+/**
+ * ID for the container element.
+ */
+const containerId = 'gr-hovercard-container';
+
+export function getHovercardContainer(
+ options: {createIfNotExists: boolean} = {createIfNotExists: false}
+): HTMLElement | null {
+ let container = getRootElement().querySelector<HTMLElement>(
+ `#${containerId}`
+ );
+ if (!container && options.createIfNotExists) {
+ // If it does not exist, create and initialize the hovercard container.
+ container = document.createElement('div');
+ container.setAttribute('id', containerId);
+ getRootElement().appendChild(container);
+ }
+ return container;
+}
+
+/**
+ * How long should we wait before showing the hovercard when the user hovers
+ * over the element?
+ */
+const SHOW_DELAY_MS = 550;
+
+/**
+ * How long should we wait before hiding the hovercard when the user moves from
+ * target to the hovercard.
+ *
+ * Note: this should be lower than SHOW_DELAY_MS to avoid flickering.
+ */
+const HIDE_DELAY_MS = 500;
+
+/**
+ * The mixin for hovercard behavior.
+ *
+ * @example
+ *
+ * class YourComponent extends hovercardBehaviorMixin(
+ * LitElement)
+ *
+ * @see gr-hovercard.ts
+ *
+ * // following annotations are required for polylint
+ * @lit
+ * @mixinFunction
+ */
+export const HovercardMixin = <T extends Constructor<LitElement>>(
+ superClass: T
+) => {
+ /**
+ * @lit
+ * @mixinClass
+ */
+ class Mixin extends superClass {
+ @query('#container')
+ topElement?: HTMLElement;
+
+ @property({type: Object})
+ _target: HTMLElement | null = null;
+
+ // Determines whether or not the hovercard is visible.
+ @property({type: Boolean})
+ _isShowing = false;
+
+ // The `id` of the element that the hovercard is anchored to.
+ @property({type: String})
+ for?: string;
+
+ /**
+ * The spacing between the top of the hovercard and the element it is
+ * anchored to.
+ */
+ @property({type: Number})
+ offset = 14;
+
+ /**
+ * Positions the hovercard to the top, right, bottom, left, bottom-left,
+ * bottom-right, top-left, or top-right of its content.
+ */
+ @property({type: String})
+ position = 'right';
+
+ @property({type: Object})
+ container: HTMLElement | null = null;
+
+ // Private but used in tests.
+ hideTask?: DelayedTask;
+
+ showTask?: DelayedTask;
+
+ isScheduledToShow?: boolean;
+
+ isScheduledToHide?: boolean;
+
+ static get styles() {
+ return [sharedStyles, hovercardStyles];
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ constructor(...args: any[]) {
+ super(...args);
+ // show the hovercard if mouse moves to hovercard
+ // this will cancel pending hide as well
+ this.addEventListener('mouseenter', this.show);
+ // when leave hovercard, hide it immediately
+ this.addEventListener('mouseleave', this.hide);
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ // We have to cache the target because when we this.container.appendChild
+ // in show we can not pick the container as target when we reconnect.
+ if (!this._target) {
+ this._target = this.target;
+ this.addTargetEventListeners();
+ }
+
+ this.container = getHovercardContainer({createIfNotExists: true});
+ }
+
+ override disconnectedCallback() {
+ this.cancelShowTask();
+ this.cancelHideTask();
+ super.disconnectedCallback();
+ }
+
+ private addTargetEventListeners() {
+ this._target?.addEventListener('mouseenter', this.debounceShow);
+ this._target?.addEventListener('focus', this.debounceShow);
+ this._target?.addEventListener('mouseleave', this.debounceHide);
+ this._target?.addEventListener('blur', this.debounceHide);
+ this._target?.addEventListener('click', this.hide);
+ }
+
+ private removeTargetEventListeners() {
+ this._target?.removeEventListener('mouseenter', this.debounceShow);
+ this._target?.removeEventListener('focus', this.debounceShow);
+ this._target?.removeEventListener('mouseleave', this.debounceHide);
+ this._target?.removeEventListener('blur', this.debounceHide);
+ this._target?.removeEventListener('click', this.hide);
+ }
+
+ /**
+ * Responds to a change in the `for` value and gets the updated `target`
+ * element for the hovercard.
+ */
+ override updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+ if (changedProperties.has('for')) {
+ this.removeTargetEventListeners();
+ this._target = this.target;
+ this.addTargetEventListeners();
+ }
+ }
+
+ readonly debounceHide = () => {
+ this.cancelShowTask();
+ if (!this._isShowing || this.isScheduledToHide) return;
+ this.isScheduledToHide = true;
+ this.hideTask = debounce(
+ this.hideTask,
+ () => {
+ // This happens when hide immediately through click or mouse leave
+ // on the hovercard
+ if (!this.isScheduledToHide) return;
+ this.hide();
+ },
+ HIDE_DELAY_MS
+ );
+ };
+
+ cancelHideTask() {
+ if (!this.hideTask) return;
+ this.hideTask.cancel();
+ this.isScheduledToHide = false;
+ this.hideTask = undefined;
+ }
+
+ /**
+ * Hovercard elements are created outside of <gr-app>, so if you want to fire
+ * events, then you probably want to do that through the target element.
+ */
+
+ dispatchEventThroughTarget(eventName: string): void;
+
+ dispatchEventThroughTarget(
+ eventName: 'show-alert',
+ detail: ShowAlertEventDetail
+ ): void;
+
+ dispatchEventThroughTarget(
+ eventName: 'reload',
+ detail: ReloadEventDetail
+ ): void;
+
+ dispatchEventThroughTarget(eventName: string, detail?: unknown) {
+ if (!detail) detail = {};
+ if (this._target)
+ this._target.dispatchEvent(
+ new CustomEvent(eventName, {
+ detail,
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ /**
+ * Returns the target element that the hovercard is anchored to (the `id` of
+ * the `for` property).
+ */
+ get target(): HTMLElement {
+ const parentNode = this.parentNode;
+ // If the parentNode is a document fragment, then we need to use the host.
+ const ownerRoot = this.getRootNode() as ShadowRoot;
+ let target;
+ if (this.for) {
+ target = ownerRoot.querySelector('#' + this.for);
+ } else {
+ target =
+ !parentNode || parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
+ ? ownerRoot.host
+ : parentNode;
+ }
+ return target as HTMLElement;
+ }
+
+ /**
+ * Hides/closes the hovercard. This occurs when the user triggers the
+ * `mouseleave` event on the hovercard's `target` element (as long as the
+ * user is not hovering over the hovercard).
+ *
+ */
+ readonly hide = (e?: MouseEvent) => {
+ this.cancelHideTask();
+ this.cancelShowTask();
+ if (!this._isShowing) {
+ return;
+ }
+
+ // If the user is now hovering over the hovercard or the user is returning
+ // from the hovercard but now hovering over the target (to stop an annoying
+ // flicker effect), just return.
+ if (e) {
+ if (
+ e.relatedTarget === this ||
+ (e.target === this && e.relatedTarget === this._target)
+ ) {
+ return;
+ }
+ }
+
+ // Mark that the hovercard is not visible and do not allow focusing
+ this._isShowing = false;
+
+ // Clear styles in preparation for the next time we need to show the card
+ this.classList.remove(HOVER_CLASS);
+
+ // Reset and remove the hovercard from the DOM
+ this.style.cssText = '';
+ this.topElement?.setAttribute('tabindex', '-1');
+
+ // Remove the hovercard from the container, given that it is still a child
+ // of the container.
+ if (this.container?.contains(this)) {
+ this.container.removeChild(this);
+ }
+ };
+
+ /**
+ * Shows/opens the hovercard with a fixed delay.
+ */
+ readonly debounceShow = () => {
+ this.debounceShowBy(SHOW_DELAY_MS);
+ };
+
+ /**
+ * Shows/opens the hovercard with the given delay.
+ */
+ debounceShowBy(delayMs: number) {
+ this.cancelHideTask();
+ if (this._isShowing || this.isScheduledToShow) return;
+ this.isScheduledToShow = true;
+ this.showTask = debounce(
+ this.showTask,
+ () => {
+ // This happens when the mouse leaves the target before the delay is over.
+ if (!this.isScheduledToShow) return;
+ this.show();
+ },
+ delayMs
+ );
+ }
+
+ cancelShowTask() {
+ if (!this.showTask) return;
+ this.showTask.cancel();
+ this.isScheduledToShow = false;
+ this.showTask = undefined;
+ }
+
+ /**
+ * Shows/opens the hovercard. This occurs when the user triggers the
+ * `mousenter` event on the hovercard's `target` element.
+ */
+ readonly show = async () => {
+ this.cancelHideTask();
+ this.cancelShowTask();
+ if (this._isShowing || !this.container) {
+ return;
+ }
+
+ // Mark that the hovercard is now visible
+ this._isShowing = true;
+ this.setAttribute('tabindex', '0');
+
+ // Add it to the DOM and calculate its position
+ this.container.appendChild(this);
+ // We temporarily hide the hovercard until we have found the correct
+ // position for it.
+ this.classList.add(HIDE_CLASS);
+ this.classList.add(HOVER_CLASS);
+ // Make sure that the hovercard actually rendered and all dom-if
+ // statements processed, so that we can measure the (invisible)
+ // hovercard properly in updatePosition().
+ await new Promise<void>(r => {
+ setTimeout(r, 0);
+ });
+ this.updatePosition();
+ this.classList.remove(HIDE_CLASS);
+ };
+
+ updatePosition() {
+ const positionsToTry = new Set([
+ this.position,
+ 'right',
+ 'bottom-right',
+ 'top-right',
+ 'bottom',
+ 'top',
+ 'bottom-left',
+ 'top-left',
+ 'left',
+ ]);
+ for (const position of positionsToTry) {
+ this.updatePositionTo(position);
+ if (this._isInsideViewport()) return;
+ }
+ console.warn('Could not find a visible position for the hovercard.');
+ }
+
+ _isInsideViewport() {
+ const thisRect = this.getBoundingClientRect();
+ if (thisRect.top < 0) return false;
+ if (thisRect.left < 0) return false;
+ const docuRect = document.documentElement.getBoundingClientRect();
+ if (thisRect.bottom > docuRect.height) return false;
+ if (thisRect.right > docuRect.width) return false;
+ return true;
+ }
+
+ /**
+ * Updates the hovercard's position based the current position of the `target`
+ * element.
+ *
+ * The hovercard is supposed to stay open if the user hovers over it.
+ * To keep it open when the user moves away from the target, the bounding
+ * rects of the target and hovercard must touch or overlap.
+ *
+ * NOTE: You do not need to directly call this method unless you need to
+ * update the position of the tooltip while it is already visible (the
+ * target element has moved and the tooltip is still open).
+ */
+ updatePositionTo(position: string) {
+ if (!this._target) {
+ return;
+ }
+
+ // Make sure that thisRect will not get any paddings and such included
+ // in the width and height of the bounding client rect.
+ this.style.cssText = '';
+
+ const docuRect = document.documentElement.getBoundingClientRect();
+ const targetRect = this._target.getBoundingClientRect();
+ const thisRect = this.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - docuRect.left;
+ const targetTop = targetRect.top - docuRect.top;
+
+ let hovercardLeft;
+ let hovercardTop;
+
+ switch (position) {
+ case 'top':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop - thisRect.height - this.offset;
+ break;
+ case 'bottom':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop + targetRect.height + this.offset;
+ break;
+ case 'left':
+ hovercardLeft = targetLeft - thisRect.width - this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ break;
+ case 'right':
+ hovercardLeft = targetLeft + targetRect.width + this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ break;
+ case 'bottom-right':
+ hovercardLeft = targetLeft + targetRect.width + this.offset;
+ hovercardTop = targetTop;
+ break;
+ case 'bottom-left':
+ hovercardLeft = targetLeft - thisRect.width - this.offset;
+ hovercardTop = targetTop;
+ break;
+ case 'top-left':
+ hovercardLeft = targetLeft - thisRect.width - this.offset;
+ hovercardTop = targetTop + targetRect.height - thisRect.height;
+ break;
+ case 'top-right':
+ hovercardLeft = targetLeft + targetRect.width + this.offset;
+ hovercardTop = targetTop + targetRect.height - thisRect.height;
+ break;
+ }
+
+ this.style.left = `${hovercardLeft}px`;
+ this.style.top = `${hovercardTop}px`;
+ }
+ }
+
+ return Mixin as T & Constructor<HovercardMixinInterface>;
+};
+
+export interface HovercardMixinInterface {
+ for?: string;
+ offset: number;
+ _target: HTMLElement | null;
+ _isShowing: boolean;
+ dispatchEventThroughTarget(eventName: string, detail?: unknown): void;
+ show(): void;
+
+ // Used for tests
+ hide(e: MouseEvent): void;
+ container: HTMLElement | null;
+ hideTask?: DelayedTask;
+ showTask?: DelayedTask;
+ position: string;
+ debounceShowBy(delayMs: number): void;
+ updatePosition(): void;
+ isScheduledToShow?: boolean;
+ isScheduledToHide?: boolean;
+}
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
new file mode 100644
index 0000000..bd12789
--- /dev/null
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -0,0 +1,177 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../test/common-test-setup-karma.js';
+import {HovercardMixin} from './hovercard-mixin.js';
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators';
+import {MockPromise, mockPromise} from '../../test/test-utils.js';
+
+const base = HovercardMixin(LitElement);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'hovercard-mixin-test': HovercardMixinTest;
+ }
+}
+
+@customElement('hovercard-mixin-test')
+class HovercardMixinTest extends base {
+ constructor() {
+ super();
+ this.for = 'foo';
+ }
+
+ override render() {
+ return html`<div id="container"><slot></slot></div>`;
+ }
+}
+
+const basicFixture = fixtureFromElement('hovercard-mixin-test');
+
+suite('gr-hovercard tests', () => {
+ let element: HovercardMixinTest;
+
+ let button: HTMLElement;
+ let testPromise: MockPromise;
+
+ setup(() => {
+ testPromise = mockPromise();
+ button = document.createElement('button');
+ button.innerHTML = 'Hello';
+ button.setAttribute('id', 'foo');
+ document.body.appendChild(button);
+
+ element = basicFixture.instantiate();
+ });
+
+ teardown(() => {
+ element.hide(new MouseEvent('click'));
+ button?.remove();
+ });
+
+ test('updatePosition', async () => {
+ // Test that the correct style properties have at least been set.
+ element.position = 'bottom';
+ element.updatePosition();
+ await element.updateComplete;
+ assert.typeOf(element.style.getPropertyValue('left'), 'string');
+ assert.typeOf(element.style.getPropertyValue('top'), 'string');
+ assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
+ assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
+
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = element!._target!.getBoundingClientRect();
+ const thisRect = element.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ const pixelCompare = (pixel: string) =>
+ Math.round(parseInt(pixel.substring(0, pixel.length - 1), 10));
+
+ assert.equal(
+ pixelCompare(element.style.left),
+ pixelCompare(`${targetLeft + (targetRect.width - thisRect.width) / 2}px`)
+ );
+ assert.equal(
+ pixelCompare(element.style.top),
+ pixelCompare(`${targetTop + targetRect.height + element.offset}px`)
+ );
+ });
+
+ test('hide', () => {
+ element.hide(new MouseEvent('click'));
+ const style = getComputedStyle(element);
+ assert.isFalse(element._isShowing);
+ assert.isFalse(element.classList.contains('hovered'));
+ assert.equal(style.display, 'none');
+ assert.notEqual(element.container, element.parentNode);
+ });
+
+ test('show', async () => {
+ await element.show();
+ await element.updateComplete;
+ const style = getComputedStyle(element);
+ assert.isTrue(element._isShowing);
+ assert.isTrue(element.classList.contains('hovered'));
+ assert.equal(style.opacity, '1');
+ assert.equal(style.visibility, 'visible');
+ });
+
+ test('debounceShow does not show immediately', async () => {
+ element.debounceShowBy(100);
+ setTimeout(() => testPromise.resolve(), 0);
+ await testPromise;
+ assert.isFalse(element._isShowing);
+ });
+
+ test('debounceShow shows after delay', async () => {
+ element.debounceShowBy(1);
+ setTimeout(() => testPromise.resolve(), 10);
+ await testPromise;
+ assert.isTrue(element._isShowing);
+ });
+
+ test('card is scheduled to show on enter and hides on leave', async () => {
+ const button = document.querySelector('button');
+ const enterPromise = mockPromise();
+ button!.addEventListener('mouseenter', () => enterPromise.resolve());
+ const leavePromise = mockPromise();
+ button!.addEventListener('mouseleave', () => leavePromise.resolve());
+
+ assert.isFalse(element._isShowing);
+ button!.dispatchEvent(new CustomEvent('mouseenter'));
+
+ await enterPromise;
+ await flush();
+ assert.isTrue(element.isScheduledToShow);
+ element!.showTask!.flush();
+ assert.isTrue(element._isShowing);
+ assert.isFalse(element.isScheduledToShow);
+
+ button!.dispatchEvent(new CustomEvent('mouseleave'));
+
+ await leavePromise;
+ assert.isTrue(element.isScheduledToHide);
+ assert.isTrue(element._isShowing);
+ element!.hideTask!.flush();
+ assert.isFalse(element.isScheduledToShow);
+ assert.isFalse(element._isShowing);
+ });
+
+ test('card should disappear on click', async () => {
+ const button = document.querySelector('button');
+ const enterPromise = mockPromise();
+ const clickPromise = mockPromise();
+ button!.addEventListener('mouseenter', () => enterPromise.resolve());
+ button!.addEventListener('click', () => clickPromise.resolve());
+
+ assert.isFalse(element._isShowing);
+
+ button!.dispatchEvent(new CustomEvent('mouseenter'));
+
+ await enterPromise;
+ await flush();
+ assert.isTrue(element.isScheduledToShow);
+ button!.click();
+
+ await clickPromise;
+ assert.isFalse(element.isScheduledToShow);
+ assert.isFalse(element._isShowing);
+ });
+});
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 4be6241..2ad4e79 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -38,7 +38,7 @@
"ba-linkify": "^1.0.1",
"codemirror-minified": "^5.62.2",
"immer": "^9.0.5",
- "lit": "2.0.0-rc.3",
+ "lit": "2.0.2",
"page": "^1.11.6",
"polymer-bridges": "file:../../polymer-bridges/",
"polymer-resin": "^2.0.1",
diff --git a/polygerrit-ui/app/styles/gr-hovercard-styles.ts b/polygerrit-ui/app/styles/gr-hovercard-styles.ts
new file mode 100644
index 0000000..f214a9c
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-hovercard-styles.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {css} from 'lit';
+
+export const hovercardStyles = css`
+ :host {
+ position: absolute;
+ display: none;
+ z-index: 200;
+ max-width: 600px;
+ outline: none;
+ }
+ :host(.hovered) {
+ display: block;
+ }
+ :host(.hide) {
+ visibility: hidden;
+ }
+ /* You have to use a <div class="container"> in your hovercard in order
+ to pick up this consistent styling. */
+ #container {
+ background: var(--dialog-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-hovercard-styles">
+ <template>
+ <style>
+ ${hovercardStyles.cssText}
+ </style>
+ </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 395ef64..bfca566 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,10 +2,10 @@
# yarn lockfile v1
-"@lit/reactive-element@^1.0.0-rc.2":
- version "1.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.0-rc.3.tgz#5032f493fbf39781b187a7e2dd5d256537c8760c"
- integrity sha512-Rs2px1keOQUNJUo5B+WExl5v244ZNCiN/iMVNO9evFdJjAdWCIupR/p14zRPkNHsciRBELLTcOZ379cI9O6PDg==
+"@lit/reactive-element@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294"
+ integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ==
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.5"
@@ -425,10 +425,10 @@
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz#d8e6c2f830e2650dc06fe74464472ff64b54a302"
integrity sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg==
-"@types/trusted-types@^1.0.1":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
- integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+"@types/trusted-types@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
+ integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@webcomponents/shadycss@^1.10.2", "@webcomponents/shadycss@^1.9.1":
version "1.11.0"
@@ -473,9 +473,9 @@
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
are-we-there-yet@~1.1.2:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
- integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
+ integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==
dependencies:
delegates "^1.0.0"
readable-stream "^2.0.6"
@@ -518,9 +518,9 @@
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror-minified@^5.62.2:
- version "5.62.2"
- resolved "https://registry.yarnpkg.com/codemirror-minified/-/codemirror-minified-5.62.2.tgz#37d866f5f39bbd4482c60b1607c669bcb7190388"
- integrity sha512-lQpyiEaqyEln1YDiHqq8lJcX8GkTJamecZAn0DkgdteFIVCRHnVmllOXPF+d159OSNkMi1UcKRObcU6ueBHe1A==
+ version "5.63.0"
+ resolved "https://registry.yarnpkg.com/codemirror-minified/-/codemirror-minified-5.63.0.tgz#29d1a78713a633c933a27853679afdc0bfea49cc"
+ integrity sha512-dMN2w0Qg5Zwn2p7UW3sYAoyrJ+QRBkiF5bfbQAvQ1bfqhEjGnZ++/zvOG7NivfnUbYRhSULz8lsFtzt4ldBNyQ==
concat-map@0.0.1:
version "0.0.1"
@@ -533,9 +533,9 @@
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
core-util-is@~1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
- integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
debug@4:
version "4.3.2"
@@ -588,9 +588,9 @@
wide-align "^1.1.0"
glob@^7.1.3:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+ integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -613,9 +613,9 @@
debug "4"
immer@^9.0.5:
- version "9.0.5"
- resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec"
- integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==
+ version "9.0.6"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
+ integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
inflight@^1.0.4:
version "1.0.6"
@@ -652,29 +652,29 @@
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-lit-element@^3.0.0-rc.2:
- version "3.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.0-rc.3.tgz#cece8f092d28eb6f9c6b23e4138ff5d7260897ef"
- integrity sha512-NDe7yjW18gfYQb1GIEQr1T8sB1GUAb1HB62pdAEw+SK6lUW7OFPKQqCOlRhZ6qJXsw9KxMnyYIprLZT4FZdYdQ==
+lit-element@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4"
+ integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-html "^2.0.0"
-lit-html@^2.0.0-rc.4:
- version "2.0.0-rc.4"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.4.tgz#1015fa8f1f7c8c5b79999ed0bc11c3b79ff1aab5"
- integrity sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==
+lit-html@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95"
+ integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w==
dependencies:
- "@types/trusted-types" "^1.0.1"
+ "@types/trusted-types" "^2.0.2"
-lit@2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.0-rc.3.tgz#8b6a85268aba287c11125dfe57e88e0bc09beaff"
- integrity sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==
+lit@2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
+ integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-element "^3.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-element "^3.0.0"
+ lit-html "^2.0.0"
lru-cache@^6.0.0:
version "6.0.0"
@@ -703,9 +703,9 @@
brace-expansion "^1.1.7"
minipass@^3.0.0:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
- integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732"
+ integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==
dependencies:
yallist "^4.0.0"
@@ -733,9 +733,11 @@
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
node-fetch@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
- integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
+ integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
+ dependencies:
+ whatwg-url "^5.0.0"
nopt@^5.0.0:
version "5.0.0"
@@ -863,9 +865,9 @@
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
signal-exit@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
- integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
+ integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
simple-concat@^1.0.0:
version "1.0.1"
@@ -920,9 +922,9 @@
ansi-regex "^3.0.0"
tar@^6.1.0:
- version "6.1.8"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.8.tgz#4fc50cfe56511c538ce15b71e05eebe66530cbd4"
- integrity sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==
+ version "6.1.11"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
+ integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
@@ -931,6 +933,11 @@
mkdirp "^1.0.3"
yallist "^4.0.0"
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+
tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -941,6 +948,19 @@
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"