Merge changes from topic "gr-confirm-revert-submission-dialog-to-ts"
* changes:
Convert gr-confirm-revert-submission-dialog to typescript
Rename files to preserve history
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index ff9fb3c..9064b97 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -106,6 +106,8 @@
List<RobotCommentInfo> robotCommentsAsList() throws RestApiException;
+ Map<String, List<CommentInfo>> portedComments() throws RestApiException;
+
/**
* Applies the indicated fix by creating a new change edit or integrating the fix with the
* existing change edit. If no change edit exists before this call, the fix must refer to the
@@ -294,6 +296,11 @@
}
@Override
+ public Map<String, List<CommentInfo>> portedComments() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public EditInfo applyFix(String fixId) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
index 923cfc3..c34e439 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
@@ -27,6 +27,7 @@
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.truth.ListSubject;
+import java.sql.Timestamp;
import java.util.List;
public class CommentInfoSubject extends Subject {
@@ -91,6 +92,10 @@
return check("inReplyTo").that(commentInfo().inReplyTo);
}
+ public StringSubject commitId() {
+ return check("commitId").that(commentInfo().commitId);
+ }
+
public AccountInfoSubject author() {
return check("author").about(accounts()).that(commentInfo().author);
}
@@ -99,6 +104,14 @@
return check("tag").that(commentInfo().tag);
}
+ public ComparableSubject<Timestamp> updated() {
+ return check("updated").that(commentInfo().updated);
+ }
+
+ public StringSubject changeMessageId() {
+ return check("changeMessageId").that(commentInfo().changeMessageId);
+ }
+
private CommentInfo commentInfo() {
isNotNull();
return commentInfo;
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index b515dfe..df01d8a 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -79,6 +79,7 @@
import com.google.gerrit.server.restapi.change.GetPatch;
import com.google.gerrit.server.restapi.change.GetRelated;
import com.google.gerrit.server.restapi.change.GetRevisionActions;
+import com.google.gerrit.server.restapi.change.ListPortedComments;
import com.google.gerrit.server.restapi.change.ListRevisionComments;
import com.google.gerrit.server.restapi.change.ListRevisionDrafts;
import com.google.gerrit.server.restapi.change.ListRobotComments;
@@ -130,6 +131,7 @@
private final FileApiImpl.Factory fileApi;
private final ListRevisionComments listComments;
private final ListRobotComments listRobotComments;
+ private final ListPortedComments listPortedComments;
private final ApplyFix applyFix;
private final GetFixPreview getFixPreview;
private final Fixes fixes;
@@ -175,6 +177,7 @@
FileApiImpl.Factory fileApi,
ListRevisionComments listComments,
ListRobotComments listRobotComments,
+ ListPortedComments listPortedComments,
ApplyFix applyFix,
GetFixPreview getFixPreview,
Fixes fixes,
@@ -219,6 +222,7 @@
this.listComments = listComments;
this.robotComments = robotComments;
this.listRobotComments = listRobotComments;
+ this.listPortedComments = listPortedComments;
this.applyFix = applyFix;
this.getFixPreview = getFixPreview;
this.fixes = fixes;
@@ -454,6 +458,15 @@
}
@Override
+ public Map<String, List<CommentInfo>> portedComments() throws RestApiException {
+ try {
+ return listPortedComments.apply(revision).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot retrieve ported comments", e);
+ }
+ }
+
+ @Override
public EditInfo applyFix(String fixId) throws RestApiException {
try {
return applyFix.apply(fixes.parse(revision, IdString.fromDecoded(fixId)), null).value();
diff --git a/java/com/google/gerrit/server/patch/DiffMappings.java b/java/com/google/gerrit/server/patch/DiffMappings.java
new file mode 100644
index 0000000..e215e77
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/DiffMappings.java
@@ -0,0 +1,52 @@
+// 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;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.patch.GitPositionTransformer.FileMapping;
+import com.google.gerrit.server.patch.GitPositionTransformer.Mapping;
+import com.google.gerrit.server.patch.GitPositionTransformer.Range;
+import com.google.gerrit.server.patch.GitPositionTransformer.RangeMapping;
+
+/** Mappings derived from diffs. */
+public class DiffMappings {
+
+ private DiffMappings() {}
+
+ public static Mapping toMapping(PatchListEntry patchListEntry) {
+ // This is just a direct translation of the former logic in EditTransformer. It doesn't
+ // work for file deletions, though. As file deletions aren't relevant for 'edits due to rebase'
+ // situations, we didn't notice this in the past.
+ // TODO(aliceks): Fix for file deletions in another change.
+ FileMapping fileMapping =
+ FileMapping.create(getOldFilePath(patchListEntry), patchListEntry.getNewName());
+ ImmutableSet<RangeMapping> rangeMappings =
+ patchListEntry.getEdits().stream()
+ .map(
+ edit ->
+ RangeMapping.create(
+ Range.create(edit.getBeginA(), edit.getEndA()),
+ Range.create(edit.getBeginB(), edit.getEndB())))
+ .collect(toImmutableSet());
+ return Mapping.create(fileMapping, rangeMappings);
+ }
+
+ private static String getOldFilePath(PatchListEntry patchListEntry) {
+ return MoreObjects.firstNonNull(patchListEntry.getOldName(), patchListEntry.getNewName());
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/EditTransformer.java b/java/com/google/gerrit/server/patch/EditTransformer.java
index 90f442e..facfad59 100644
--- a/java/com/google/gerrit/server/patch/EditTransformer.java
+++ b/java/com/google/gerrit/server/patch/EditTransformer.java
@@ -15,19 +15,20 @@
package com.google.gerrit.server.patch;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Multimaps.toMultimap;
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
-import java.util.ArrayList;
+import com.google.gerrit.server.patch.GitPositionTransformer.Mapping;
+import com.google.gerrit.server.patch.GitPositionTransformer.Position;
+import com.google.gerrit.server.patch.GitPositionTransformer.PositionedEntity;
+import com.google.gerrit.server.patch.GitPositionTransformer.Range;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
@@ -105,76 +106,23 @@
}
private void transformEdits(List<PatchListEntry> transformingEntries, SideStrategy sideStrategy) {
- Map<String, List<ContextAwareEdit>> editsPerFilePath =
- edits.stream().collect(groupingBy(sideStrategy::getFilePath));
- Map<String, List<PatchListEntry>> transEntriesPerPath =
- transformingEntries.stream().collect(groupingBy(EditTransformer::getOldFilePath));
+ ImmutableList<PositionedEntity<ContextAwareEdit>> positionedEdits =
+ edits.stream()
+ .map(edit -> toPositionedEntity(edit, sideStrategy))
+ .collect(toImmutableList());
+ ImmutableSet<Mapping> mappings =
+ transformingEntries.stream().map(DiffMappings::toMapping).collect(toImmutableSet());
edits =
- editsPerFilePath.entrySet().stream()
- .flatMap(
- pathAndEdits -> {
- List<PatchListEntry> transEntries =
- transEntriesPerPath.getOrDefault(pathAndEdits.getKey(), ImmutableList.of());
- return transformEdits(sideStrategy, pathAndEdits.getValue(), transEntries);
- })
- .collect(toList());
+ GitPositionTransformer.transform(positionedEdits, mappings).stream()
+ .map(PositionedEntity::getEntityAtUpdatedPosition)
+ .collect(toImmutableList());
}
- private static String getOldFilePath(PatchListEntry patchListEntry) {
- return MoreObjects.firstNonNull(patchListEntry.getOldName(), patchListEntry.getNewName());
- }
-
- private static Stream<ContextAwareEdit> transformEdits(
- SideStrategy sideStrategy,
- List<ContextAwareEdit> originalEdits,
- List<PatchListEntry> transformingEntries) {
- if (transformingEntries.isEmpty()) {
- return originalEdits.stream();
- }
-
- // TODO(aliceks): Find a way to prevent an explosion of the number of entries.
- return transformingEntries.stream()
- .flatMap(
- transEntry ->
- transformEdits(
- sideStrategy, originalEdits, transEntry.getEdits(), transEntry.getNewName())
- .stream());
- }
-
- private static List<ContextAwareEdit> transformEdits(
- SideStrategy sideStrategy,
- List<ContextAwareEdit> unorderedOriginalEdits,
- List<Edit> unorderedTransformingEdits,
- String adjustedFilePath) {
- List<ContextAwareEdit> originalEdits = new ArrayList<>(unorderedOriginalEdits);
- originalEdits.sort(comparing(sideStrategy::getBegin).thenComparing(sideStrategy::getEnd));
- List<Edit> transformingEdits = new ArrayList<>(unorderedTransformingEdits);
- transformingEdits.sort(comparing(Edit::getBeginA).thenComparing(Edit::getEndA));
-
- int shiftedAmount = 0;
- int transIndex = 0;
- int origIndex = 0;
- List<ContextAwareEdit> resultingEdits = new ArrayList<>(originalEdits.size());
- while (origIndex < originalEdits.size() && transIndex < transformingEdits.size()) {
- ContextAwareEdit originalEdit = originalEdits.get(origIndex);
- Edit transformingEdit = transformingEdits.get(transIndex);
- if (transformingEdit.getEndA() <= sideStrategy.getBegin(originalEdit)) {
- shiftedAmount = transformingEdit.getEndB() - transformingEdit.getEndA();
- transIndex++;
- } else if (sideStrategy.getEnd(originalEdit) <= transformingEdit.getBeginA()) {
- resultingEdits.add(sideStrategy.create(originalEdit, shiftedAmount, adjustedFilePath));
- origIndex++;
- } else {
- // Overlapping -> ignore.
- origIndex++;
- }
- }
- for (int i = origIndex; i < originalEdits.size(); i++) {
- resultingEdits.add(
- sideStrategy.create(originalEdits.get(i), shiftedAmount, adjustedFilePath));
- }
- return resultingEdits;
+ private static PositionedEntity<ContextAwareEdit> toPositionedEntity(
+ ContextAwareEdit edit, SideStrategy sideStrategy) {
+ return PositionedEntity.create(
+ edit, sideStrategy::extractPosition, sideStrategy::createEditAtNewPosition);
}
@AutoValue
@@ -234,44 +182,32 @@
}
private interface SideStrategy {
- String getFilePath(ContextAwareEdit edit);
+ Position extractPosition(ContextAwareEdit edit);
- int getBegin(ContextAwareEdit edit);
-
- int getEnd(ContextAwareEdit edit);
-
- ContextAwareEdit create(ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath);
+ ContextAwareEdit createEditAtNewPosition(ContextAwareEdit edit, Position newPosition);
}
private enum SideAStrategy implements SideStrategy {
INSTANCE;
@Override
- public String getFilePath(ContextAwareEdit edit) {
- return edit.getOldFilePath();
+ public Position extractPosition(ContextAwareEdit edit) {
+ return Position.builder()
+ .filePath(edit.getOldFilePath())
+ .lineRange(Range.create(edit.getBeginA(), edit.getEndA()))
+ .build();
}
@Override
- public int getBegin(ContextAwareEdit edit) {
- return edit.getBeginA();
- }
-
- @Override
- public int getEnd(ContextAwareEdit edit) {
- return edit.getEndA();
- }
-
- @Override
- public ContextAwareEdit create(
- ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) {
+ public ContextAwareEdit createEditAtNewPosition(ContextAwareEdit edit, Position newPosition) {
return ContextAwareEdit.create(
- adjustedFilePath,
+ newPosition.filePath(),
edit.getNewFilePath(),
- edit.getBeginA() + shiftedAmount,
- edit.getEndA() + shiftedAmount,
+ newPosition.lineRange().start(),
+ newPosition.lineRange().end(),
edit.getBeginB(),
edit.getEndB(),
- !Objects.equals(edit.getOldFilePath(), adjustedFilePath));
+ !Objects.equals(edit.getOldFilePath(), newPosition.filePath()));
}
}
@@ -279,31 +215,23 @@
INSTANCE;
@Override
- public String getFilePath(ContextAwareEdit edit) {
- return edit.getNewFilePath();
+ public Position extractPosition(ContextAwareEdit edit) {
+ return Position.builder()
+ .filePath(edit.getNewFilePath())
+ .lineRange(Range.create(edit.getBeginB(), edit.getEndB()))
+ .build();
}
@Override
- public int getBegin(ContextAwareEdit edit) {
- return edit.getBeginB();
- }
-
- @Override
- public int getEnd(ContextAwareEdit edit) {
- return edit.getEndB();
- }
-
- @Override
- public ContextAwareEdit create(
- ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) {
+ public ContextAwareEdit createEditAtNewPosition(ContextAwareEdit edit, Position newPosition) {
return ContextAwareEdit.create(
edit.getOldFilePath(),
- adjustedFilePath,
+ newPosition.filePath(),
edit.getBeginA(),
edit.getEndA(),
- edit.getBeginB() + shiftedAmount,
- edit.getEndB() + shiftedAmount,
- !Objects.equals(edit.getNewFilePath(), adjustedFilePath));
+ newPosition.lineRange().start(),
+ newPosition.lineRange().end(),
+ !Objects.equals(edit.getNewFilePath(), newPosition.filePath()));
}
}
}
diff --git a/java/com/google/gerrit/server/patch/GitPositionTransformer.java b/java/com/google/gerrit/server/patch/GitPositionTransformer.java
new file mode 100644
index 0000000..64241d8
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/GitPositionTransformer.java
@@ -0,0 +1,455 @@
+// 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;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.groupingBy;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Transformer of {@link Position}s in one Git tree to {@link Position}s in another Git tree given
+ * the {@link Mapping}s between the trees.
+ *
+ * <p>The base idea is that a {@link Position} in the source tree can be translated/mapped to a
+ * corresponding {@link Position} in the target tree when we know how the target tree changed
+ * compared to the source tree. As long as {@link Position}s are only defined via file path and line
+ * range, we only need to know which file path in the source tree corresponds to which file path in
+ * the target tree and how the lines within that file changed from the source to the target tree.
+ *
+ * <p>The algorithm is roughly:
+ *
+ * <ol>
+ * <li>Go over all positions and replace the file path for each of them with the corresponding one
+ * in the target tree. If a file path maps to two file paths in the target tree (copied file),
+ * duplicate the position entry and use each of the new file paths with it. If a file path
+ * maps to no file in the target tree (deleted file), drop the position.
+ * <li>Per file path, go through the file from top to bottom and keep track of how the range
+ * mappings for that file shift the lines. Derive the shifted amount by comparing the number
+ * of lines between source and target in the range mapping. While going through the file,
+ * shift each encountered position by the currently tracked amount. If a position overlaps
+ * with the lines of a range mapping, drop the position.
+ * </ol>
+ */
+public class GitPositionTransformer {
+
+ // This is currently only a utility class but it won't stay like that.
+ private GitPositionTransformer() {}
+
+ /**
+ * Transforms the {@link Position}s of the specified entities as indicated via the {@link
+ * Mapping}s.
+ *
+ * <p>This is typically used to transform the {@link Position}s in one Git tree (source) to the
+ * corresponding {@link Position}s in another Git tree (target). The {@link Mapping}s need to
+ * indicate all relevant changes between the source and target tree. {@link Mapping}s for files
+ * not referenced by the given {@link Position}s need not be specified. They can be included,
+ * though, as they aren't harmful.
+ *
+ * @param entities the entities whose {@link Position} should be mapped to the target tree
+ * @param mappings the mappings describing all relevant changes between the source and the target
+ * tree
+ * @param <T> an entity which has a {@link Position}
+ * @return a list of entities with transformed positions
+ */
+ public static <T> ImmutableList<PositionedEntity<T>> transform(
+ Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) {
+ // Update the file paths first as copied files might exist. For copied files, this operation
+ // will duplicate the PositionedEntity instances of the original file.
+ List<PositionedEntity<T>> filePathUpdatedEntities = updateFilePaths(entities, mappings);
+
+ return shiftRanges(filePathUpdatedEntities, mappings);
+ }
+
+ private static <T> ImmutableList<PositionedEntity<T>> updateFilePaths(
+ Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) {
+ Map<String, ImmutableSet<String>> newFilesPerOldFile = groupNewFilesByOldFiles(mappings);
+ return entities.stream()
+ .flatMap(entity -> mapToNewFileIfChanged(newFilesPerOldFile, entity))
+ .collect(toImmutableList());
+ }
+
+ private static Map<String, ImmutableSet<String>> groupNewFilesByOldFiles(Set<Mapping> mappings) {
+ return mappings.stream()
+ .map(Mapping::file)
+ .collect(
+ groupingBy(
+ FileMapping::oldPath, Collectors.mapping(FileMapping::newPath, toImmutableSet())));
+ }
+
+ private static <T> Stream<PositionedEntity<T>> mapToNewFileIfChanged(
+ Map<String, ? extends Set<String>> newFilesPerOldFile, PositionedEntity<T> entity) {
+ String oldFilePath = entity.position().filePath();
+ if (!newFilesPerOldFile.containsKey(oldFilePath)) {
+ // Unchanged files don't have a mapping. -> Keep existing entries.
+ return Stream.of(entity);
+ }
+ Set<String> newFiles = newFilesPerOldFile.get(oldFilePath);
+ return newFiles.stream().map(entity::withFilePath);
+ }
+
+ private static <T> ImmutableList<PositionedEntity<T>> shiftRanges(
+ List<PositionedEntity<T>> filePathUpdatedEntities, Set<Mapping> mappings) {
+ Map<String, ImmutableSet<RangeMapping>> mappingsPerNewFilePath =
+ groupRangeMappingsByNewFilePath(mappings);
+ Map<String, ImmutableList<PositionedEntity<T>>> entitiesPerNewFilePath =
+ groupByFilePath(filePathUpdatedEntities);
+ return entitiesPerNewFilePath.entrySet().stream()
+ .flatMap(
+ newFilePathAndEntities ->
+ shiftRangesInOneFileIfChanged(
+ mappingsPerNewFilePath,
+ newFilePathAndEntities.getKey(),
+ newFilePathAndEntities.getValue())
+ .stream())
+ .collect(toImmutableList());
+ }
+
+ private static Map<String, ImmutableSet<RangeMapping>> groupRangeMappingsByNewFilePath(
+ Set<Mapping> mappings) {
+ return mappings.stream()
+ .collect(
+ groupingBy(
+ mapping -> mapping.file().newPath(),
+ collectingAndThen(
+ Collectors.<Mapping, Set<RangeMapping>>reducing(
+ new HashSet<>(), Mapping::ranges, Sets::union),
+ ImmutableSet::copyOf)));
+ }
+
+ private static <T> Map<String, ImmutableList<PositionedEntity<T>>> groupByFilePath(
+ List<PositionedEntity<T>> fileUpdatedEntities) {
+ return fileUpdatedEntities.stream()
+ .collect(groupingBy(entity -> entity.position().filePath(), toImmutableList()));
+ }
+
+ private static <T> ImmutableList<PositionedEntity<T>> shiftRangesInOneFileIfChanged(
+ Map<String, ImmutableSet<RangeMapping>> mappingsPerNewFilePath,
+ String newFilePath,
+ ImmutableList<PositionedEntity<T>> sameFileEntities) {
+ ImmutableSet<RangeMapping> sameFileRangeMappings =
+ mappingsPerNewFilePath.getOrDefault(newFilePath, ImmutableSet.of());
+ if (sameFileRangeMappings.isEmpty()) {
+ // Unchanged files and pure renames/copies don't have range mappings. -> Keep existing
+ // entries.
+ return sameFileEntities;
+ }
+ return shiftRangesInOneFile(sameFileEntities, sameFileRangeMappings);
+ }
+
+ private static <T> ImmutableList<PositionedEntity<T>> shiftRangesInOneFile(
+ List<PositionedEntity<T>> sameFileEntities, Set<RangeMapping> sameFileRangeMappings) {
+ ImmutableList<PositionedEntity<T>> sortedEntities = sortByStartEnd(sameFileEntities);
+ ImmutableList<RangeMapping> sortedMappings = sortByOldStartEnd(sameFileRangeMappings);
+
+ int shiftedAmount = 0;
+ int mappingIndex = 0;
+ int entityIndex = 0;
+ ImmutableList.Builder<PositionedEntity<T>> resultingEntities =
+ ImmutableList.builderWithExpectedSize(sortedEntities.size());
+ while (entityIndex < sortedEntities.size() && mappingIndex < sortedMappings.size()) {
+ PositionedEntity<T> entity = sortedEntities.get(entityIndex);
+ RangeMapping mapping = sortedMappings.get(mappingIndex);
+ if (mapping.oldLineRange().end() <= entity.position().lineRange().start()) {
+ shiftedAmount = mapping.newLineRange().end() - mapping.oldLineRange().end();
+ mappingIndex++;
+ } else if (entity.position().lineRange().end() <= mapping.oldLineRange().start()) {
+ resultingEntities.add(entity.shiftPositionBy(shiftedAmount));
+ entityIndex++;
+ } else {
+ // Overlapping -> ignore.
+ entityIndex++;
+ }
+ }
+ for (int i = entityIndex; i < sortedEntities.size(); i++) {
+ resultingEntities.add(sortedEntities.get(i).shiftPositionBy(shiftedAmount));
+ }
+ return resultingEntities.build();
+ }
+
+ private static <T> ImmutableList<PositionedEntity<T>> sortByStartEnd(
+ List<PositionedEntity<T>> entities) {
+ return entities.stream()
+ .sorted(
+ comparing(
+ entity -> entity.position().lineRange(),
+ comparing(Range::start).thenComparing(Range::end)))
+ .collect(toImmutableList());
+ }
+
+ private static ImmutableList<RangeMapping> sortByOldStartEnd(Set<RangeMapping> mappings) {
+ return mappings.stream()
+ .sorted(
+ comparing(
+ RangeMapping::oldLineRange, comparing(Range::start).thenComparing(Range::end)))
+ .collect(toImmutableList());
+ }
+
+ /**
+ * A mapping from a {@link Position} in one Git commit/tree (source) to a {@link Position} in
+ * another Git commit/tree (target).
+ */
+ @AutoValue
+ public abstract static class Mapping {
+
+ /** A mapping describing how the attributes of one file are mapped from source to target. */
+ public abstract FileMapping file();
+
+ /**
+ * Mappings describing how line ranges within the file indicated by {@link #file()} are mapped
+ * from source to target.
+ */
+ public abstract ImmutableSet<RangeMapping> ranges();
+
+ public static Mapping create(FileMapping fileMapping, Iterable<RangeMapping> rangeMappings) {
+ return new AutoValue_GitPositionTransformer_Mapping(
+ fileMapping, ImmutableSet.copyOf(rangeMappings));
+ }
+ }
+
+ /**
+ * A mapping of attributes from a file in one Git tree (source) to a file in another Git tree
+ * (target).
+ *
+ * <p>At the moment, only the file path is considered. Other attributes like file mode would be
+ * imaginable too but are currently not supported.
+ */
+ @AutoValue
+ public abstract static class FileMapping {
+
+ /** File path in the source tree. */
+ public abstract String oldPath();
+
+ /** File path in the target tree. Can be the same as {@link #oldPath()}. */
+ public abstract String newPath();
+
+ /**
+ * Creates a new {@code FileMapping}.
+ *
+ * @param oldPath see {@link #oldPath()}
+ * @param newPath see {@link #newPath()}
+ */
+ public static FileMapping create(String oldPath, String newPath) {
+ return new AutoValue_GitPositionTransformer_FileMapping(oldPath, newPath);
+ }
+ }
+
+ /**
+ * A mapping of a line range in one Git tree (source) to the corresponding line range in another
+ * Git tree (target).
+ */
+ @AutoValue
+ public abstract static class RangeMapping {
+
+ /** Range in the source tree. */
+ public abstract Range oldLineRange();
+
+ /** Range in the target tree. */
+ public abstract Range newLineRange();
+
+ /**
+ * Creates a new {@code RangeMapping}.
+ *
+ * @param oldRange see {@link #oldLineRange()}
+ * @param newRange see {@link #newLineRange()}
+ */
+ public static RangeMapping create(Range oldRange, Range newRange) {
+ return new AutoValue_GitPositionTransformer_RangeMapping(oldRange, newRange);
+ }
+ }
+
+ /**
+ * A position within the tree of a Git commit.
+ *
+ * <p>The term 'position' is our own invention. The underlying idea is that a Gerrit comment is at
+ * a specific position within the commit of a patchset. That position is defined by the attributes
+ * defined in this class.
+ *
+ * <p>The same thinking can be applied to diff hunks (= JGit edits). Each diff hunk maps a
+ * position in one commit (e.g. in the parent of the patchset) to a position in another commit
+ * (e.g. in the commit of the patchset).
+ *
+ * <p>We only refer to lines and not character offsets within the lines here as Git only works
+ * with line precision. In theory, we could do better in Gerrit as we also have intraline diffs.
+ * Incorporating those requires careful considerations, though.
+ */
+ @AutoValue
+ public abstract static class Position {
+
+ /** Absolute file path. */
+ public abstract String filePath();
+
+ /** Affected lines. */
+ public abstract Range lineRange();
+
+ /**
+ * Creates a copy of this {@code Position} whose range is shifted by the indicated amount.
+ *
+ * @param amount number of lines to shift. Negative values mean moving the range up, positive
+ * values mean moving the range down.
+ * @return a new {@code Position} instance with an updated range
+ */
+ public Position shiftBy(int amount) {
+ return toBuilder().lineRange(lineRange().shiftBy(amount)).build();
+ }
+
+ /**
+ * Creates a copy of this {@code Position} whose file path is adjusted to the indicated value.
+ *
+ * @param filePath the new file path to use
+ * @return a new {@code Position} instance with an update file path
+ */
+ public Position withFilePath(String filePath) {
+ return toBuilder().filePath(filePath).build();
+ }
+
+ abstract Builder toBuilder();
+
+ public static Builder builder() {
+ return new AutoValue_GitPositionTransformer_Position.Builder();
+ }
+
+ /** Builder of a {@link Position}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ /** See {@link #filePath()}. */
+ public abstract Builder filePath(String filePath);
+
+ /** See {@link #lineRange()}. */
+ public abstract Builder lineRange(Range lineRange);
+
+ public abstract Position build();
+ }
+ }
+
+ /** A range. In the context of {@link GitPositionTransformer}, this is a line range. */
+ @AutoValue
+ public abstract static class Range {
+
+ /** Start of the range. (inclusive) */
+ public abstract int start();
+
+ /** End of the range. (exclusive) */
+ public abstract int end();
+
+ /**
+ * Creates a copy of this {@code Range} which is shifted by the indicated amount. A shift
+ * equally applies to both {@link #start()} end {@link #end()}.
+ *
+ * @param amount amount to shift. Negative values mean moving the range up, positive values mean
+ * moving the range down.
+ * @return a new {@code Range} instance with updated start/end
+ */
+ public Range shiftBy(int amount) {
+ return create(start() + amount, end() + amount);
+ }
+
+ public static Range create(int start, int end) {
+ return new AutoValue_GitPositionTransformer_Range(start, end);
+ }
+ }
+
+ /**
+ * Wrapper around an instance of {@code T} which annotates it with a {@link Position}. Methods
+ * such as {@link #shiftPositionBy(int)} and {@link #withFilePath(String)} allow to update the
+ * associated {@link Position}. Afterwards, use {@link #getEntityAtUpdatedPosition()} to get an
+ * updated version of the {@code T} instance.
+ *
+ * @param <T> an object/entity type which has a {@link Position}
+ */
+ public static class PositionedEntity<T> {
+
+ private final T entity;
+ private final Position position;
+ private final BiFunction<T, Position, T> updatedEntityCreator;
+
+ /**
+ * Creates a new {@code PositionedEntity}.
+ *
+ * @param entity an instance which should be annotated with a {@link Position}
+ * @param positionExtractor a function describing how a {@link Position} can be derived from the
+ * given entity
+ * @param updatedEntityCreator a function to create a new entity of type {@code T} from an
+ * existing entity and a given {@link Position}. This must return a new instance of type
+ * {@code T}! The existing instance must not be modified!
+ * @param <T> an object/entity type which has a {@link Position}
+ */
+ public static <T> PositionedEntity<T> create(
+ T entity,
+ Function<T, Position> positionExtractor,
+ BiFunction<T, Position, T> updatedEntityCreator) {
+ Position position = positionExtractor.apply(entity);
+ return new PositionedEntity<>(entity, position, updatedEntityCreator);
+ }
+
+ private PositionedEntity(
+ T entity, Position position, BiFunction<T, Position, T> updatedEntityCreator) {
+ this.entity = entity;
+ this.position = position;
+ this.updatedEntityCreator = updatedEntityCreator;
+ }
+
+ /**
+ * Returns an updated version of the entity to which the internally stored {@link Position} was
+ * written back to.
+ *
+ * @return an updated instance of {@code T}
+ */
+ public T getEntityAtUpdatedPosition() {
+ return updatedEntityCreator.apply(entity, position);
+ }
+
+ Position position() {
+ return position;
+ }
+
+ /**
+ * Shifts the tracked {@link Position} by the specified amount.
+ *
+ * @param amount number of lines to shift. Negative values mean moving the range up, positive
+ * values mean moving the range down.
+ * @return a {@code PositionedEntity} with updated {@link Position}
+ */
+ public PositionedEntity<T> shiftPositionBy(int amount) {
+ return new PositionedEntity<>(entity, position.shiftBy(amount), updatedEntityCreator);
+ }
+
+ /**
+ * Updates the file path of the tracked {@link Position}.
+ *
+ * @param filePath the new file path to use
+ * @return a {@code PositionedEntity} with updated {@link Position}
+ */
+ public PositionedEntity<T> withFilePath(String filePath) {
+ return new PositionedEntity<>(entity, position.withFilePath(filePath), updatedEntityCreator);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ListPortedComments.java b/java/com/google/gerrit/server/restapi/change/ListPortedComments.java
new file mode 100644
index 0000000..3f9e127
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/ListPortedComments.java
@@ -0,0 +1,189 @@
+// 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.restapi.change;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.stream.Collectors.groupingBy;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Comment.Range;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.DiffMappings;
+import com.google.gerrit.server.patch.GitPositionTransformer;
+import com.google.gerrit.server.patch.GitPositionTransformer.Mapping;
+import com.google.gerrit.server.patch.GitPositionTransformer.Position;
+import com.google.gerrit.server.patch.GitPositionTransformer.PositionedEntity;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListPortedComments implements RestReadView<RevisionResource> {
+
+ private final CommentsUtil commentsUtil;
+ private final Provider<CommentJson> commentJson;
+ private final PatchListCache patchListCache;
+
+ @Inject
+ public ListPortedComments(
+ Provider<CommentJson> commentJson, CommentsUtil commentsUtil, PatchListCache patchListCache) {
+ this.commentJson = commentJson;
+ this.commentsUtil = commentsUtil;
+ this.patchListCache = patchListCache;
+ }
+
+ @Override
+ public Response<Map<String, List<CommentInfo>>> apply(RevisionResource revisionResource)
+ throws PermissionBackendException, PatchListNotAvailableException {
+ PatchSet targetPatchset = revisionResource.getPatchSet();
+
+ List<HumanComment> allComments = loadAllPublishedComments(revisionResource);
+ ImmutableList<HumanComment> relevantComments = filterToRelevant(allComments, targetPatchset);
+ ImmutableList<HumanComment> portedComments =
+ portToTargetPatchset(
+ revisionResource.getChangeResource().getNotes(), targetPatchset, relevantComments);
+ return Response.ok(format(portedComments));
+ }
+
+ private List<HumanComment> loadAllPublishedComments(RevisionResource revisionResource) {
+ return commentsUtil.publishedHumanCommentsByChange(revisionResource.getNotes());
+ }
+
+ private ImmutableList<HumanComment> filterToRelevant(
+ List<HumanComment> allComments, PatchSet targetPatchset) {
+ return allComments.stream()
+ .filter(comment -> comment.key.patchSetId < targetPatchset.number())
+ // TODO(aliceks): Also support comments on parent revisions.
+ .filter(comment -> comment.side > 0)
+ .collect(toImmutableList());
+ }
+
+ private ImmutableList<HumanComment> portToTargetPatchset(
+ ChangeNotes notes, PatchSet targetPatchset, List<HumanComment> comments)
+ throws PatchListNotAvailableException {
+ Map<Integer, ImmutableList<HumanComment>> commentsPerPatchset =
+ comments.stream().collect(groupingBy(comment -> comment.key.patchSetId, toImmutableList()));
+
+ ImmutableList.Builder<HumanComment> portedComments =
+ ImmutableList.builderWithExpectedSize(comments.size());
+ for (Integer originalPatchsetId : commentsPerPatchset.keySet()) {
+ ImmutableList<HumanComment> patchsetComments = commentsPerPatchset.get(originalPatchsetId);
+ PatchSet originalPatchset =
+ notes.getPatchSets().get(PatchSet.id(notes.getChangeId(), originalPatchsetId));
+ portedComments.addAll(
+ portToTargetPatchset(
+ notes.getProjectName(), originalPatchset, targetPatchset, patchsetComments));
+ }
+ return portedComments.build();
+ }
+
+ private ImmutableList<HumanComment> portToTargetPatchset(
+ Project.NameKey project,
+ PatchSet originalPatchset,
+ PatchSet targetPatchset,
+ ImmutableList<HumanComment> comments)
+ throws PatchListNotAvailableException {
+ ImmutableSet<Mapping> mappings =
+ loadPatchsetMappings(project, originalPatchset, targetPatchset);
+ ImmutableList<PositionedEntity<HumanComment>> positionedComments =
+ comments.stream().map(this::toPositionedEntity).collect(toImmutableList());
+ return GitPositionTransformer.transform(positionedComments, mappings).stream()
+ .map(PositionedEntity::getEntityAtUpdatedPosition)
+ .collect(toImmutableList());
+ }
+
+ private ImmutableSet<Mapping> loadPatchsetMappings(
+ Project.NameKey project, PatchSet originalPatchset, PatchSet targetPatchset)
+ throws PatchListNotAvailableException {
+ PatchList patchList =
+ patchListCache.get(
+ PatchListKey.againstCommit(
+ originalPatchset.commitId(), targetPatchset.commitId(), Whitespace.IGNORE_NONE),
+ project);
+ return patchList.getPatches().stream().map(DiffMappings::toMapping).collect(toImmutableSet());
+ }
+
+ private PositionedEntity<HumanComment> toPositionedEntity(HumanComment comment) {
+ return PositionedEntity.create(
+ comment,
+ ListPortedComments::extractPosition,
+ ListPortedComments::createCommentAtNewPosition);
+ }
+
+ private static Position extractPosition(HumanComment comment) {
+ Position.Builder positionBuilder = Position.builder().filePath(comment.key.filename);
+ return positionBuilder.lineRange(extractLineRange(comment)).build();
+ }
+
+ private static GitPositionTransformer.Range extractLineRange(HumanComment comment) {
+ // Line specifications in comment are 1-based. Line specifications in Position are 0-based.
+ if (comment.range != null) {
+ // The combination of (line, charOffset) is exclusive and must be mapped to an exclusive line.
+ int exclusiveEndLine =
+ comment.range.endChar > 0 ? comment.range.endLine : comment.range.endLine - 1;
+ return GitPositionTransformer.Range.create(comment.range.startLine - 1, exclusiveEndLine);
+ }
+ return GitPositionTransformer.Range.create(comment.lineNbr - 1, comment.lineNbr);
+ }
+
+ private static HumanComment createCommentAtNewPosition(
+ HumanComment originalComment, Position newPosition) {
+ HumanComment portedComment = new HumanComment(originalComment);
+ portedComment.key.filename = newPosition.filePath();
+ portedComment.lineNbr = newPosition.lineRange().start() + 1;
+ if (portedComment.range != null) {
+ portedComment.range =
+ toRange(
+ newPosition.lineRange(), portedComment.range.startChar, portedComment.range.endChar);
+ portedComment.lineNbr = portedComment.range.endLine;
+ }
+ return portedComment;
+ }
+
+ private static Comment.Range toRange(
+ GitPositionTransformer.Range lineRange, int originalStartChar, int originalEndChar) {
+ int adjustedEndLine = originalEndChar > 0 ? lineRange.end() : lineRange.end() + 1;
+ return new Range(lineRange.start() + 1, originalStartChar, adjustedEndLine, originalEndChar);
+ }
+
+ private Map<String, List<CommentInfo>> format(List<HumanComment> comments)
+ throws PermissionBackendException {
+ return commentJson
+ .get()
+ .setFillAccounts(true)
+ .setFillPatchSet(true)
+ .newHumanCommentFormatter()
+ .format(comments);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 466ea3c..1a4f270 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -172,6 +172,8 @@
post(FIX_KIND, "apply").to(ApplyFix.class);
get(FIX_KIND, "preview").to(GetFixPreview.class);
+ get(REVISION_KIND, "ported_comments").to(ListPortedComments.class);
+
child(REVISION_KIND, "files").to(Files.class);
put(FILE_KIND, "reviewed").to(PutReviewed.class);
delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 4d77c60..c7fe74e 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -82,6 +82,16 @@
command(git, "upload-pack").to(Upload.class);
command("git-upload-archive").to(Commands.key(git, "upload-archive"));
command(git, "upload-archive").to(UploadArchive.class);
+ if (slaveMode) {
+ command("git-receive-pack").to(ReceiveSlaveMode.class);
+ command("gerrit-receive-pack").to(ReceiveSlaveMode.class);
+ command(git, "receive-pack").to(ReceiveSlaveMode.class);
+ } else {
+ command("git-receive-pack").to(Commands.key(git, "receive-pack"));
+ command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
+ command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
+ command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
+ }
}
command("suexec").to(SuExec.class);
listener().to(ShowCaches.StartupListener.class);
@@ -91,18 +101,6 @@
command(gerrit, CreateProjectCommand.class);
command(gerrit, SetHeadCommand.class);
- if (slaveMode) {
- command("git-receive-pack").to(ReceiveSlaveMode.class);
- command("gerrit-receive-pack").to(ReceiveSlaveMode.class);
- command(git, "receive-pack").to(ReceiveSlaveMode.class);
- } else {
- if (sshEnabled()) {
- command("git-receive-pack").to(Commands.key(git, "receive-pack"));
- command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
- command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
- }
- command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
- }
command(gerrit, Receive.class);
command(gerrit, RenameGroupCommand.class);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
new file mode 100644
index 0000000..7e844b6
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
@@ -0,0 +1,1547 @@
+// 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.acceptance.api.revision;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.collect.MoreCollectors.onlyElement;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommentInfoSubject.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommentInfoSubject.assertThatList;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestPatchset;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.truth.NullAwareCorrespondence;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class PortedCommentsIT extends AbstractDaemonTest {
+
+ @Inject private ChangeOperations changeOps;
+ @Inject private AccountOperations accountOps;
+ @Inject private RequestScopeOperations requestScopeOps;
+
+ @Test
+ public void onlyCommentsBeforeTargetPatchsetArePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ PatchSet.Id patchset3Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String comment1Uuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+ changeOps.change(changeId).patchset(patchset2Id).newComment().create();
+ changeOps.change(changeId).patchset(patchset3Id).newComment().create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThatList(portedComments).comparingElementsUsing(hasUuid()).containsExactly(comment1Uuid);
+ }
+
+ @Test
+ public void commentsOnAnyPatchsetBeforeTargetPatchsetArePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ changeOps.change(changeId).newPatchset().create();
+ PatchSet.Id patchset3Id = changeOps.change(changeId).newPatchset().create();
+ PatchSet.Id patchset4Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String comment1Uuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+ String comment3Uuid = changeOps.change(changeId).patchset(patchset3Id).newComment().create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset4Id));
+
+ assertThat(portedComments)
+ .comparingElementsUsing(hasUuid())
+ .containsExactly(comment1Uuid, comment3Uuid);
+ }
+
+ @Test
+ public void severalCommentsFromEarlierPatchsetArePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String comment1Uuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+ String comment2Uuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments)
+ .comparingElementsUsing(hasUuid())
+ .containsExactly(comment1Uuid, comment2Uuid);
+ }
+
+ @Test
+ public void completeCommentThreadIsPorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String rootCommentUuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+ String child1CommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .parentUuid(rootCommentUuid)
+ .create();
+ String child2CommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .parentUuid(child1CommentUuid)
+ .create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments)
+ .comparingElementsUsing(hasUuid())
+ .containsExactly(rootCommentUuid, child1CommentUuid, child2CommentUuid);
+ }
+
+ @Test
+ // TODO(aliceks): Filter out unresolved comment threads.
+ @Ignore
+ public void onlyUnresolvedCommentsArePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ changeOps.change(changeId).patchset(patchset1Id).newComment().resolved().create();
+ String comment2Uuid =
+ changeOps.change(changeId).patchset(patchset1Id).newComment().unresolved().create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(comment2Uuid);
+ }
+
+ @Test
+ // TODO(aliceks): Filter out unresolved comment threads.
+ @Ignore
+ public void unresolvedStateOfLastCommentInThreadMatters() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String rootComment1Uuid =
+ changeOps.change(changeId).patchset(patchset1Id).newComment().resolved().create();
+ String childComment1Uuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .parentUuid(rootComment1Uuid)
+ .unresolved()
+ .create();
+ String rootComment2Uuid =
+ changeOps.change(changeId).patchset(patchset1Id).newComment().unresolved().create();
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .parentUuid(rootComment2Uuid)
+ .resolved()
+ .create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments)
+ .comparingElementsUsing(hasUuid())
+ .containsExactly(rootComment1Uuid, childComment1Uuid);
+ }
+
+ @Test
+ public void unresolvedStateOfLastCommentByDateMattersForBranchedThreads() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String rootCommentUuid =
+ changeOps.change(changeId).patchset(patchset1Id).newComment().resolved().create();
+ String childComment1Uuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .parentUuid(rootCommentUuid)
+ .resolved()
+ .create();
+ String childComment2Uuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .parentUuid(rootCommentUuid)
+ .unresolved()
+ .create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments)
+ .comparingElementsUsing(hasUuid())
+ .containsExactly(rootCommentUuid, childComment1Uuid, childComment2Uuid);
+ }
+
+ @Test
+ public void draftCommentsAreNotPortedViaApiForPublishedComments() throws Exception {
+ Account.Id accountId = accountOps.newAccount().create();
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add draft comment.
+ changeOps.change(changeId).patchset(patchset1Id).newDraftComment().author(accountId).create();
+
+ // Draft comments are only visible to their author.
+ requestScopeOps.setApiUser(accountId);
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThatList(portedComments).isEmpty();
+ }
+
+ @Test
+ public void publishedCommentsOfAllTypesArePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String rangeCommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .message("Range comment")
+ .fromLine(1)
+ .charOffset(2)
+ .toLine(2)
+ .charOffset(1)
+ .ofFile("myFile")
+ .create();
+ String lineCommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .message("Line comment")
+ .onLine(1)
+ .ofFile("myFile")
+ .create();
+ String fileCommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .message("File comment")
+ .onFileLevelOf("myFile")
+ .create();
+ String patchsetLevelCommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .message("Patchset-level comment")
+ .onPatchsetLevel()
+ .create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments)
+ .comparingElementsUsing(hasUuid())
+ .containsExactly(
+ rangeCommentUuid, lineCommentUuid, fileCommentUuid, patchsetLevelCommentUuid);
+ }
+
+ // This is not the desired behavior but at least a current state which doesn't throw exceptions
+ // or has wrong behavior.
+ @Test
+ public void commentOnParentCommitIsIgnored() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ changeOps.change(changeId).patchset(patchset1Id).newComment().onParentCommit().create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThat(portedComments).isEmpty();
+ }
+
+ @Test
+ public void portedCommentHasOriginalUuid() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+
+ assertThatList(portedComments).onlyElement().uuid().isEqualTo(commentUuid);
+ }
+
+ @Test
+ public void portedCommentHasOriginalPatchset() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).patchSet().isEqualTo(patchset1Id.get());
+ }
+
+ @Test
+ public void portedCommentHasOriginalPatchsetCommitId() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid =
+ changeOps.change(changeId).patchset(patchset1.patchsetId()).newComment().create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).commitId().isEqualTo(patchset1.commitId().name());
+ }
+
+ @Test
+ public void portedCommentHasOriginalMessage() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1.patchsetId())
+ .newComment()
+ .message("My comment text")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).message().isEqualTo("My comment text");
+ }
+
+ @Test
+ public void portedReplyStillRefersToParentComment() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comments.
+ String rootCommentUuid =
+ changeOps.change(changeId).patchset(patchset1.patchsetId()).newComment().create();
+ String childCommentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1.patchsetId())
+ .newComment()
+ .parentUuid(rootCommentUuid)
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, childCommentUuid);
+
+ assertThat(portedComment).inReplyTo().isEqualTo(rootCommentUuid);
+ }
+
+ @Test
+ public void portedCommentHasOriginalAuthor() throws Exception {
+ // Set up change and patchsets.
+ Account.Id authorId = accountOps.newAccount().create();
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid =
+ changeOps.change(changeId).patchset(patchset1Id).newComment().author(authorId).create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).author().id().isEqualTo(authorId.get());
+ }
+
+ @Test
+ public void portedCommentHasOriginalTag() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1.patchsetId())
+ .newComment()
+ .tag("My comment tag")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).tag().isEqualTo("My comment tag");
+ }
+
+ @Test
+ public void portedCommentHasUpdatedTimestamp() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).updated().isNotNull();
+ }
+
+ @Test
+ public void portedCommentDoesNotHaveChangeMessageId() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid = changeOps.change(changeId).patchset(patchset1Id).newComment().create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ // There's currently no use case for linking ported comments to specific change messages. Hence,
+ // there's no reason to fill this field, which requires additional computations.
+ // Besides, we also don't fill this field for the comments requested for a specific patchset.
+ assertThat(portedComment).changeMessageId().isNull();
+ }
+
+ @Test
+ public void pathOfPortedCommentIsOnlyIndicatedInMap() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onFileLevelOf("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("myFile");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).path().isNull();
+ }
+
+ @Test
+ public void portedRangeCommentCanHandleAddedLines() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().startLine().isEqualTo(5);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(6);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ public void portedRangeCommentCanHandleDeletedLines() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().startLine().isEqualTo(2);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(3);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ public void portedRangeCommentCanHandlePureRename() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("newFileName");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).range().startLine().isEqualTo(3);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(4);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ public void portedRangeCommentCanHandleRenameWithLineShift() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .delete()
+ .file("newFileName")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("newFileName");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).range().startLine().isEqualTo(5);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(6);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ public void portedRangeCommentAdditionallyAppearsOnCopyAtIndependentPosition() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ // Gerrit currently only identifies a copy if a rename also happens at the same time. Modify the
+ // renamed file slightly different than the copied file so that the end location of the comment
+ // is different. Modify the renamed file less so that Gerrit/Git picks it as the renamed one.
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .delete()
+ .file("renamedFiled")
+ .content("Line 1\nLine 1.1\nLine 2\nLine 3\nLine 4\n")
+ .file("copiedFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("renamedFiled", "copiedFile");
+ CommentInfo portedCommentOnRename = getOnlyElement(portedComments.get("renamedFiled"));
+ assertThat(portedCommentOnRename).uuid().isEqualTo(commentUuid);
+ assertThat(portedCommentOnRename).range().startLine().isEqualTo(4);
+ assertThat(portedCommentOnRename).range().startCharacter().isEqualTo(2);
+ assertThat(portedCommentOnRename).range().endLine().isEqualTo(5);
+ assertThat(portedCommentOnRename).range().endCharacter().isEqualTo(5);
+ CommentInfo portedCommentOnCopy = getOnlyElement(portedComments.get("copiedFile"));
+ assertThat(portedCommentOnCopy).uuid().isEqualTo(commentUuid);
+ assertThat(portedCommentOnCopy).range().startLine().isEqualTo(5);
+ assertThat(portedCommentOnCopy).range().startCharacter().isEqualTo(2);
+ assertThat(portedCommentOnCopy).range().endLine().isEqualTo(6);
+ assertThat(portedCommentOnCopy).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ public void lineOfPortedRangeCommentFollowsContract() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ // Line is equal to the end line, which is at 6 when ported.
+ assertThat(portedComment).line().isEqualTo(6);
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedRangeCommentBecomesFileCommentOnConflict() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine two\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(2)
+ .charOffset(2)
+ .toLine(3)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("myFile");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedRangeCommentEndingOnLineJustBeforeModificationCanBePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(1)
+ .charOffset(2)
+ .toLine(2)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().startLine().isEqualTo(1);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(2);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ public void portedRangeCommentEndingAtStartOfModifiedLineCanBePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(1)
+ .charOffset(2)
+ .toLine(3)
+ .charOffset(0)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().startLine().isEqualTo(1);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(3);
+ assertThat(portedComment).range().endCharacter().isEqualTo(0);
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedRangeCommentEndingWithinModifiedLineBecomesFileComment() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(1)
+ .charOffset(2)
+ .toLine(3)
+ .charOffset(4)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedRangeCommentWithinModifiedLineBecomesFileComment() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(3)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedRangeCommentStartingWithinLastModifiedLineBecomesFileComment()
+ throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line one\nLine two\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(2)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedRangeCommentStartingOnLineJustAfterModificationCanBePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine two\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().startLine().isEqualTo(3);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(4);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and only map start/end conflicts to file
+ // comments.
+ @Ignore
+ public void portedRangeCommentStartingBeforeButEndingAfterModifiedLineCanBePorted()
+ throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(2)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).range().startLine().isEqualTo(3);
+ assertThat(portedComment).range().startCharacter().isEqualTo(2);
+ assertThat(portedComment).range().endLine().isEqualTo(4);
+ assertThat(portedComment).range().endCharacter().isEqualTo(5);
+ }
+
+ @Test
+ // TODO(aliceks): Correct handling of deleted files.
+ @Ignore
+ public void portedRangeCommentBecomesPatchsetLevelCommentOnFileDeletion() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").delete().create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .fromLine(3)
+ .charOffset(2)
+ .toLine(4)
+ .charOffset(5)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+ assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedLineCommentCanHandleAddedLines() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isEqualTo(5);
+ }
+
+ @Test
+ public void portedLineCommentCanHandleDeletedLines() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isEqualTo(2);
+ }
+
+ @Test
+ public void portedLineCommentCanHandlePureRename() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("newFileName");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).line().isEqualTo(3);
+ }
+
+ @Test
+ public void portedLineCommentCanHandleRenameWithLineShift() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ PatchSet.Id patchset3Id =
+ changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset3Id);
+
+ assertThatMap(portedComments).keys().containsExactly("newFileName");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).line().isEqualTo(5);
+ }
+
+ @Test
+ public void portedLineCommentAdditionallyAppearsOnCopyAtIndependentPosition() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ // Gerrit currently only identifies a copy if a rename also happens at the same time. Modify the
+ // renamed file slightly different than the copied file so that the end location of the comment
+ // is different. Modify the renamed file less so that Gerrit/Git picks it as the renamed one.
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .delete()
+ .file("renamedFiled")
+ .content("Line 1\nLine 1.1\nLine 2\nLine 3\nLine 4\n")
+ .file("copiedFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("renamedFiled", "copiedFile");
+ CommentInfo portedCommentOnRename = getOnlyElement(portedComments.get("renamedFiled"));
+ assertThat(portedCommentOnRename).uuid().isEqualTo(commentUuid);
+ assertThat(portedCommentOnRename).line().isEqualTo(4);
+ CommentInfo portedCommentOnCopy = getOnlyElement(portedComments.get("copiedFile"));
+ assertThat(portedCommentOnCopy).uuid().isEqualTo(commentUuid);
+ assertThat(portedCommentOnCopy).line().isEqualTo(5);
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedLineCommentBecomesFileCommentOnConflict() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine two\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(2)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("myFile");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedLineCommentOnLineJustBeforeModificationCanBePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(2)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isEqualTo(2);
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedLineCommentOnStartLineOfModificationBecomesFileComment() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nSome completely\ndifferent\ncontent\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ // TODO(aliceks): Adjust code to not ignore conflicts and add mapping to file comments.
+ @Ignore
+ public void portedLineCommentOnLastLineOfModificationBecomesFileComment() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nSome completely\ndifferent\ncontent\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(4)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedLineCommentOnLineJustAfterModificationCanBePorted() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 2\nLine three\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(4)
+ .ofFile("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isEqualTo(4);
+ }
+
+ @Test
+ // TODO(aliceks): Correct handling of deleted files.
+ @Ignore
+ public void portedLineCommentBecomesPatchsetLevelCommentOnFileDeletion() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").delete().create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onLine(3)
+ .ofFile("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+ assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedFileCommentIsObliviousToAdjustedFileContent() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onFileLevelOf("myFile")
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedFileCommentCanHandleRename() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onFileLevelOf("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("newFileName");
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedFileCommentAdditionallyAppearsOnCopy() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .renameTo("renamedFiled")
+ .file("copiedFile")
+ .content("Line 1\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ changeOps.change(changeId).patchset(patchset1Id).newComment().onFileLevelOf("myFile").create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly("renamedFiled", "copiedFile");
+ CommentInfo portedCommentOnCopy = getOnlyElement(portedComments.get("copiedFile"));
+ assertThat(portedCommentOnCopy).line().isNull();
+ }
+
+ @Test
+ // TODO(aliceks): Correct handling of deleted files.
+ @Ignore
+ public void portedFileCommentBecomesPatchsetLevelCommentOnFileDeletion() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").delete().create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ .onFileLevelOf("myFile")
+ .create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+ assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+ CommentInfo portedComment = extractSpecificComment(portedComments, commentUuid);
+ assertThat(portedComment).range().isNull();
+ assertThat(portedComment).line().isNull();
+ }
+
+ @Test
+ public void portedPatchsetLevelCommentIsObliviousToAdjustedFileContent() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .file("myFile")
+ .content("Line 1\nLine 1.1\nLine 1.2\nLine 2\nLine 3\nLine 4\n")
+ .create();
+ // Add comment.
+ changeOps.change(changeId).patchset(patchset1Id).newComment().onPatchsetLevel().create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+ }
+
+ @Test
+ public void portedPatchsetLevelCommentIsObliviousToRename() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
+ // Add comment.
+ changeOps.change(changeId).patchset(patchset1Id).newComment().onPatchsetLevel().create();
+
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchset2Id);
+
+ assertThatMap(portedComments).keys().containsExactly(Patch.PATCHSET_LEVEL);
+ }
+
+ @Test
+ public void lineCommentOnCommitMessageIsPortedToNewPosition() throws Exception {
+ // Set up change and patchsets.
+ Change.Id changeId =
+ changeOps.newChange().commitMessage("Summary line\n\nText 1\nText 2").create();
+ PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id patchset2Id =
+ changeOps
+ .change(changeId)
+ .newPatchset()
+ .commitMessage("Summary line\n\nText 1\nText 1.1\nText 2")
+ .create();
+ // Add comment.
+ String commentUuid =
+ changeOps
+ .change(changeId)
+ .patchset(patchset1Id)
+ .newComment()
+ // The /COMMIT_MSG file has a header of 6 lines, so the summary line is in line 7.
+ // Place comment on 'Text 2' which is line 10.
+ .onLine(10)
+ .ofFile(Patch.COMMIT_MSG)
+ .create();
+
+ CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
+
+ assertThat(portedComment).line().isEqualTo(11);
+ }
+
+ private CommentInfo getPortedComment(PatchSet.Id patchsetId, String commentUuid)
+ throws RestApiException {
+ Map<String, List<CommentInfo>> portedComments = getPortedComments(patchsetId);
+ return extractSpecificComment(portedComments, commentUuid);
+ }
+
+ private Map<String, List<CommentInfo>> getPortedComments(PatchSet.Id patchsetId)
+ throws RestApiException {
+ return gApi.changes()
+ .id(patchsetId.changeId().get())
+ .revision(patchsetId.get())
+ .portedComments();
+ }
+
+ private static CommentInfo extractSpecificComment(
+ Map<String, List<CommentInfo>> portedComments, String commentUuid) {
+ return portedComments.values().stream()
+ .flatMap(Collection::stream)
+ .filter(comment -> comment.id.equals(commentUuid))
+ .collect(onlyElement());
+ }
+
+ /**
+ * Returns all comments in one list. The map keys (= file paths) are simply ignored. The returned
+ * comments won't have the file path attribute set for them as they came from a map with that
+ * attribute as key (= established Gerrit behavior).
+ */
+ private static ImmutableList<CommentInfo> flatten(
+ Map<String, List<CommentInfo>> commentsPerFile) {
+ return commentsPerFile.values().stream()
+ .flatMap(Collection::stream)
+ .collect((toImmutableList()));
+ }
+
+ // Unfortunately, we don't get an absolutely helpful error message when using this correspondence
+ // as CommentInfo doesn't have a toString() implementation. Even if we added it, the string
+ // representation would be quite unwieldy due to the huge number of comment attributes.
+ // Interestingly, using Correspondence#formattingDiffsUsing didn't improve anything.
+ private static Correspondence<CommentInfo, String> hasUuid() {
+ return NullAwareCorrespondence.transforming(comment -> comment.id, "hasUuid");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index df5bfca..6aa5878 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -39,6 +39,7 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.CachedProjectConfig;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
@@ -63,6 +64,7 @@
@Inject private ProjectOperations projectOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private RequestScopeOperations requestScopeOperations;
@Before
public void setUp() throws Exception {
@@ -204,6 +206,28 @@
}
@Test
+ public void customLabelMaxWithBlock_DeletedVoteDoesNotTriggerNegativeBlock() throws Exception {
+ saveLabelConfig(P.toBuilder().setFunction(MAX_WITH_BLOCK));
+ PushOneCommit.Result r = createChange();
+ requestScopeOperations.setApiUser(user.id());
+ revision(r).review(new ReviewInput().label(P_LABEL_NAME, 1));
+ requestScopeOperations.setApiUser(admin.id());
+ revision(r).review(new ReviewInput().label(P_LABEL_NAME, 1));
+
+ LabelInfo labelInfo = getWithLabels(r).labels.get(P_LABEL_NAME);
+ assertThat(labelInfo.all).hasSize(2);
+ assertThat(labelInfo.approved).isNotNull();
+ assertThat(labelInfo.blocking).isNull();
+
+ revision(r).reviewer(admin.email()).deleteVote(P_LABEL_NAME);
+ labelInfo = getWithLabels(r).labels.get(P_LABEL_NAME);
+ assertThat(labelInfo.all).hasSize(2); // 0 vote still delivered
+ assertThat(labelInfo.approved).isNotNull();
+ assertThat(labelInfo.rejected).isNull();
+ assertThat(labelInfo.blocking).isNull(); // label is not blocking the change submission
+ }
+
+ @Test
public void customLabelMaxWithBlock_MaxVoteSubmittable() throws Exception {
saveLabelConfig(
LABEL.toBuilder().setFunction(MAX_WITH_BLOCK), P.toBuilder().setFunction(NO_OP));
diff --git a/plugins/replication b/plugins/replication
index 9a07d19..7ad27c7 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 9a07d19326cab1dccbab5696c31f89dc8cb2e8a6
+Subproject commit 7ad27c7ac49731e70dd5ad3691b0b410492792f0
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index e555583..9affc1b 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -39,7 +39,7 @@
import {htmlTemplate} from './gr-admin-view_html.js';
import {getBaseUrl} from '../../../utils/url-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {getAdminLinks} from '../../../utils/admin-nav-util.js';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
@@ -112,7 +112,7 @@
reload() {
const promises = [
this.$.restAPI.getAccount(),
- pluginLoader.awaitPluginsLoaded(),
+ getPluginLoader().awaitPluginsLoaded(),
];
return Promise.all(promises).then(result => {
this._account = result[0];
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index 9ce4296..d255c68 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -19,7 +19,7 @@
import './gr-admin-view.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {stubBaseUrl} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-admin-view');
@@ -35,7 +35,7 @@
},
});
const pluginsLoaded = Promise.resolve();
- sinon.stub(pluginLoader, 'awaitPluginsLoaded').returns(pluginsLoaded);
+ sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
pluginsLoaded.then(() => flush(done));
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index e657795..673332b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -34,7 +34,7 @@
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {getDisplayName} from '../../../utils/display-name-util.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {appContext} from '../../../services/app-context.js';
import {truncatePath} from '../../../utils/path-list-util.js';
import {changeStatuses} from '../../../utils/change-util.js';
@@ -107,10 +107,11 @@
/** @override */
attached() {
super.attached();
- pluginLoader.awaitPluginsLoaded().then(() => {
- this._dynamicCellEndpoints = getPluginEndpoints().getDynamicEndpoints(
- 'change-list-item-cell');
- });
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicCellEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-list-item-cell');
+ });
}
_changeStatuses(change) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 87d1b78..2230c38 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -31,7 +31,7 @@
import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {changeIsOpen} from '../../../utils/change-util.js';
const NUMBER_FIXED_COLUMNS = 3;
@@ -166,10 +166,11 @@
/** @override */
attached() {
super.attached();
- pluginLoader.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
- 'change-list-header');
- });
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicHeaderEndpoints = getPluginEndpoints().
+ getDynamicEndpoints('change-list-header');
+ });
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 5f83a14..0285f26 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -37,7 +37,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-change-actions_html.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {appContext} from '../../../services/app-context.js';
import {
fetchChangeUpdates,
@@ -551,7 +551,8 @@
}
_handleLoadingComplete() {
- pluginLoader.awaitPluginsLoaded().then(() => this._loading = false);
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => this._loading = false);
}
_sendShowRevisionActions(detail) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index f5d2080..e163479 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -19,7 +19,7 @@
import './gr-change-actions.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {generateChange} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-change-actions');
@@ -85,7 +85,7 @@
getProjectConfig() { return Promise.resolve({}); },
});
- sinon.stub(pluginLoader, 'awaitPluginsLoaded')
+ sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
.returns(Promise.resolve());
element = basicFixture.instantiate();
@@ -2013,7 +2013,7 @@
getProjectConfig() { return Promise.resolve({}); },
});
- sinon.stub(pluginLoader, 'awaitPluginsLoaded')
+ sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
.returns(Promise.resolve());
element = basicFixture.instantiate();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
index 0f9e3b1..e812657 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
@@ -19,7 +19,7 @@
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import './gr-change-metadata.js';
import {resetPlugins} from '../../../test/test-utils.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
const testHtmlPlugin = document.createElement('dom-module');
@@ -117,10 +117,11 @@
plugin.registerStyleModule('change-metadata', 'my-plugin-style');
}, undefined, 'http://test.com/plugins/style.js');
element = createElement();
- pluginLoader.loadPlugins([]);
- pluginLoader.awaitPluginsLoaded().then(() => {
- flush(done);
- });
+ getPluginLoader().loadPlugins([]);
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => {
+ flush(done);
+ });
});
teardown(() => {
@@ -143,8 +144,8 @@
plugin = p;
plugin.registerStyleModule('change-metadata', 'my-plugin-style');
}, undefined, 'http://test.com/plugins/style.js');
- sinon.stub(pluginLoader, 'arePluginsLoaded').returns(true);
- pluginLoader.loadPlugins([]);
+ sinon.stub(getPluginLoader(), 'arePluginsLoaded').returns(true);
+ getPluginLoader().loadPlugins([]);
element = createElement();
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index ca0205c..b038bf6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -177,6 +177,7 @@
_changeChanged(change) {
this._assignee = change.assignee ? [change.assignee] : [];
+ this._settingTopic = false;
}
_assigneeChanged(assigneeRecord) {
@@ -236,8 +237,12 @@
const lastTopic = this.change.topic;
if (!topic.length) { topic = null; }
this._settingTopic = true;
- this.$.restAPI.setChangeTopic(this.change._number, topic)
+ const topicChangedForChangeNumber = this.change._number;
+ this.$.restAPI.setChangeTopic(topicChangedForChangeNumber, topic)
.then(newTopic => {
+ if (this.change._number !== topicChangedForChangeNumber) {
+ return;
+ }
this._settingTopic = false;
this.set(['change', 'topic'], newTopic);
if (newTopic !== lastTopic) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js
index 8ec3121..f7a080f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js
@@ -19,7 +19,7 @@
import '../../core/gr-router/gr-router.js';
import './gr-change-metadata.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
const basicFixture = fixtureFromElement('gr-change-metadata');
@@ -763,7 +763,7 @@
},
'0.1',
'http://some/plugins/url.html');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
flush(() => {
assert.strictEqual(hookEl.plugin, plugin);
assert.strictEqual(hookEl.change, element.change);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index b1b5a9b..7bac47d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -56,7 +56,7 @@
import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
import {PrimaryTab, SecondaryTab} from '../../../constants/constants.js';
import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
@@ -498,7 +498,7 @@
this._setDiffViewMode();
});
- pluginLoader.awaitPluginsLoaded()
+ getPluginLoader().awaitPluginsLoaded()
.then(() => {
this._dynamicTabHeaderEndpoints =
getPluginEndpoints().getDynamicEndpoints('change-view-tab-header');
@@ -1075,9 +1075,10 @@
this._performPostLoadTasks();
});
- pluginLoader.awaitPluginsLoaded().then(() => {
- this._initActiveTabs(value);
- });
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => {
+ this._initActiveTabs(value);
+ });
}
_initActiveTabs(params = {}) {
@@ -1108,6 +1109,7 @@
_performPostLoadTasks() {
this._maybeShowReplyDialog();
this._maybeShowRevertDialog();
+ this._maybeShowDownloadDialog();
this._sendShowChangeEvent();
@@ -1179,7 +1181,7 @@
}
_maybeShowRevertDialog() {
- pluginLoader.awaitPluginsLoaded()
+ getPluginLoader().awaitPluginsLoaded()
.then(this._getLoggedIn.bind(this))
.then(loggedIn => {
if (!loggedIn || !this._change ||
@@ -1208,6 +1210,13 @@
});
}
+ _maybeShowDownloadDialog() {
+ if (this.viewState.showDownloadDialog) {
+ this._handleOpenDownloadDialog();
+ this.set('viewState.showDownloadDialog', false);
+ }
+ }
+
_resetFileListViewState() {
this.set('viewState.selectedFileIndex', 0);
this.set('viewState.scrollTop', 0);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 6352238..0f80b608 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -466,6 +466,7 @@
<gr-button
link=""
class="editCommitMessage"
+ title="Edit commit message"
on-click="_handleEditCommitMessage"
hidden$="[[_hideEditCommitMessage]]"
>Edit</gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 3975be7..3d530b5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -25,7 +25,7 @@
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {EventType} from '../../plugins/gr-plugin-types.js';
@@ -301,7 +301,7 @@
});
element = fixture.instantiate();
sinon.stub(element.$.actions, 'reload').returns(Promise.resolve());
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
pluginApi.install(
plugin => {
plugin.registerDynamicCustomComponent(
@@ -1642,7 +1642,7 @@
test('revert dialog opened with revert param', done => {
sinon.stub(element.$.restAPI, 'getLoggedIn')
.callsFake(() => Promise.resolve(true));
- sinon.stub(pluginLoader, 'awaitPluginsLoaded')
+ sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
.callsFake(() => Promise.resolve());
element._patchRange = {
@@ -1671,7 +1671,7 @@
done);
element._maybeShowRevertDialog();
- assert.isTrue(pluginLoader.awaitPluginsLoaded.called);
+ assert.isTrue(getPluginLoader().awaitPluginsLoaded.called);
});
suite('scroll related tests', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index c7cab1e..9f2ff51 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -236,10 +236,7 @@
if (schemes.length === 0) {
return;
}
- if (!this._selectedScheme) {
- return;
- }
- if (!schemes.includes(this._selectedScheme)) {
+ if (!this._selectedScheme || !schemes.includes(this._selectedScheme)) {
this._selectedScheme = schemes.sort()[0];
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index beabeef..1355412 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -225,12 +225,17 @@
<gr-button
id="expandBtn"
link=""
- title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT,
- ShortcutSection.DIFFS)]]"
+ title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+ ShortcutSection.FILE_LIST)]]"
on-click="_expandAllDiffs"
>Expand All</gr-button
>
- <gr-button id="collapseBtn" link="" on-click="_collapseAllDiffs"
+ <gr-button
+ id="collapseBtn"
+ link=""
+ on-click="_collapseAllDiffs"
+ title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+ ShortcutSection.FILE_LIST)]]"
>Collapse All</gr-button
>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 06cd18a..aaf0205 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -38,7 +38,7 @@
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {appContext} from '../../../services/app-context.js';
import {SpecialFilePath} from '../../../constants/constants.js';
import {descendedFromClass} from '../../../utils/dom-util.js';
@@ -310,34 +310,35 @@
/** @override */
attached() {
super.attached();
- pluginLoader.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-header');
- this._dynamicContentEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-content');
- this._dynamicPrependedHeaderEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-header-prepend');
- this._dynamicPrependedContentEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-content-prepend');
- this._dynamicSummaryEndpoints = getPluginEndpoints()
- .getDynamicEndpoints('change-view-file-list-summary');
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicHeaderEndpoints = getPluginEndpoints()
+ .getDynamicEndpoints('change-view-file-list-header');
+ this._dynamicContentEndpoints = getPluginEndpoints()
+ .getDynamicEndpoints('change-view-file-list-content');
+ this._dynamicPrependedHeaderEndpoints = getPluginEndpoints()
+ .getDynamicEndpoints('change-view-file-list-header-prepend');
+ this._dynamicPrependedContentEndpoints = getPluginEndpoints()
+ .getDynamicEndpoints('change-view-file-list-content-prepend');
+ this._dynamicSummaryEndpoints = getPluginEndpoints()
+ .getDynamicEndpoints('change-view-file-list-summary');
- if (this._dynamicHeaderEndpoints.length !==
+ if (this._dynamicHeaderEndpoints.length !==
this._dynamicContentEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list header and content.');
- }
- if (this._dynamicPrependedHeaderEndpoints.length !==
+ console.warn(
+ 'Different number of dynamic file-list header and content.');
+ }
+ if (this._dynamicPrependedHeaderEndpoints.length !==
this._dynamicPrependedContentEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list header and content.');
- }
- if (this._dynamicHeaderEndpoints.length !==
+ console.warn(
+ 'Different number of dynamic file-list header and content.');
+ }
+ if (this._dynamicHeaderEndpoints.length !==
this._dynamicSummaryEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list headers and summary.');
- }
- });
+ console.warn(
+ 'Different number of dynamic file-list headers and summary.');
+ }
+ });
}
/** @override */
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
index 9c4ef04..d3a72072 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
@@ -68,7 +68,7 @@
role="switch"
on-tap="_onTapShowAllActivityToggle"
></paper-toggle-button>
- <div id="showAllEntriesLabel">
+ <div id="showAllEntriesLabel" aria-hidden="true">
<span>Show all entries</span>
<span class="hiddenEntries" hidden$="[[_showAllActivity]]">
([[_computeHiddenEntriesCount(_combinedMessages)]] hidden)
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
index 08921b4..7cce9e2 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
@@ -18,7 +18,7 @@
import '../../../test/common-test-setup-karma.js';
import './gr-related-changes-list.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {resetPlugins} from '../../../test/test-utils.js';
@@ -581,7 +581,7 @@
},
'0.1',
'http://some/plugins/url1.html');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
flush(() => {
assert.strictEqual(hookEl.plugin, plugin);
assert.strictEqual(hookEl.change, element.change);
@@ -605,7 +605,7 @@
},
'0.1',
'http://some/plugins/url2.html');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
flush(() => {
// No changes, and plugin without hidden attribute. So it's visible.
element._resultsChanged({}, {}, [], [], []);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 56612ff..c1272c6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -19,7 +19,7 @@
import {resetPlugins} from '../../../test/test-utils.js';
import './gr-reply-dialog.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
const basicFixture = fixtureFromElement('gr-reply-dialog');
const pluginApi = _testOnly_initGerritPluginApi();
@@ -122,20 +122,21 @@
}, null, 'http://test.com/plugins/lgtm.js');
element = basicFixture.instantiate();
setupElement(element);
- pluginLoader.loadPlugins([]);
- pluginLoader.awaitPluginsLoaded().then(() => {
- flush(() => {
- const textarea = element.$.textarea.getNativeTextarea();
- textarea.value = 'LGTM';
- textarea.dispatchEvent(new CustomEvent(
- 'input', {bubbles: true, composed: true}));
- const labelScoreRows = dom(element.$.labelScores.root)
- .querySelector('gr-label-score-row[name="Code-Review"]');
- const selectedBtn = dom(labelScoreRows.root)
- .querySelector('gr-button[data-value="+1"].iron-selected');
- assert.isOk(selectedBtn);
- done();
- });
- });
+ getPluginLoader().loadPlugins([]);
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => {
+ flush(() => {
+ const textarea = element.$.textarea.getNativeTextarea();
+ textarea.value = 'LGTM';
+ textarea.dispatchEvent(new CustomEvent(
+ 'input', {bubbles: true, composed: true}));
+ const labelScoreRows = dom(element.$.labelScores.root)
+ .querySelector('gr-label-score-row[name="Code-Review"]');
+ const selectedBtn = dom(labelScoreRows.root)
+ .querySelector('gr-button[data-value="+1"].iron-selected');
+ assert.isOk(selectedBtn);
+ done();
+ });
+ });
});
});
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index b36435e..e355e04 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -26,7 +26,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-main-header_html.js';
import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {getAdminLinks} from '../../../utils/admin-nav-util.js';
const DEFAULT_LINKS = [{
@@ -259,7 +259,7 @@
const promises = [
this.$.restAPI.getAccount(),
this.$.restAPI.getTopMenus(),
- pluginLoader.awaitPluginsLoaded(),
+ getPluginLoader().awaitPluginsLoaded(),
];
return Promise.all(promises).then(result => {
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.js b/polygerrit-ui/app/elements/custom-dark-theme_test.js
index d4f790f..ad12e14 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.js
@@ -19,7 +19,7 @@
import {getComputedStyleValue} from '../utils/dom-util.js';
import './shared/gr-rest-api-interface/gr-rest-api-interface.js';
import './gr-app.js';
-import {pluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
import {removeTheme} from '../styles/themes/dark-theme.js';
const basicFixture = fixtureFromElement('gr-app');
@@ -30,8 +30,9 @@
window.localStorage.setItem('dark-theme', 'true');
element = basicFixture.instantiate();
- pluginLoader.loadPlugins([]);
- pluginLoader.awaitPluginsLoaded().then(() => flush(done));
+ getPluginLoader().loadPlugins([]);
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => flush(done));
});
teardown(() => {
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.js b/polygerrit-ui/app/elements/custom-light-theme_test.js
index 5c0cf28..6d5b61e 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.js
@@ -19,7 +19,7 @@
import {getComputedStyleValue} from '../utils/dom-util.js';
import './shared/gr-rest-api-interface/gr-rest-api-interface.js';
import './gr-app.js';
-import {pluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
const basicFixture = fixtureFromElement('gr-app');
@@ -36,8 +36,9 @@
_fetchSharedCacheURL() { return Promise.resolve({}); },
});
element = basicFixture.instantiate();
- pluginLoader.loadPlugins([]);
- pluginLoader.awaitPluginsLoaded().then(() => flush(done));
+ getPluginLoader().loadPlugins([]);
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => flush(done));
});
teardown(() => {
// The app sends requests to server. This can lead to
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 189de4a..73ad899 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -274,6 +274,8 @@
'_handleOpenReplyDialogOrToggleLeftPane',
[Shortcut.TOGGLE_LEFT_PANE]:
'_handleOpenReplyDialogOrToggleLeftPane',
+ [Shortcut.OPEN_DOWNLOAD_DIALOG]:
+ '_handleOpenDownloadDialog',
[Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
[Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
[Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
@@ -592,6 +594,14 @@
this._navToChangeView();
}
+ _handleOpenDownloadDialog(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (this.modifierPressed(e)) { return; }
+ this.set('changeViewState.showDownloadDialog', true);
+ e.preventDefault();
+ this._navToChangeView();
+ }
+
_handleUpToChange(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 01cfe48..f0b0dcd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -50,6 +50,7 @@
kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
kb.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
kb.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
@@ -539,6 +540,10 @@
assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
'5'),
'Should navigate to /c/42/5..10');
+
+ assert.isUndefined(element.changeViewState.showDownloadDialog);
+ MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
+ assert.isTrue(element.changeViewState.showDownloadDialog);
});
test('keyboard shortcuts with old patch number', () => {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 058f476..12cd971 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -188,8 +188,9 @@
return this.$.restAPI.getFileContent(changeNum, path, patchNum)
.then(res => {
+ const content = (res && res.content) || '';
if (storedContent && storedContent.message &&
- storedContent.message !== res.content) {
+ storedContent.message !== content) {
this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: RESTORED_MESSAGE},
bubbles: true,
@@ -198,14 +199,14 @@
this._newContent = storedContent.message;
} else {
- this._newContent = res.content || '';
+ this._newContent = content;
}
- this._content = res.content || '';
+ this._content = content;
// A non-ok response may result if the file does not yet exist.
// The `type` field of the response is only valid when the file
// already exists.
- if (res.ok && res.type) {
+ if (res && res.ok && res.type) {
this._type = res.type;
} else {
this._type = '';
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 2ee3a66..a304a1b 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -230,6 +230,7 @@
patchRange: null,
selectedFileIndex: 0,
showReplyDialog: false,
+ showDownloadDialog: false,
diffMode: null,
numFilesShown: null,
scrollTop: 0,
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.js
index 7d8539d..30ff123 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -60,7 +60,7 @@
import {GrRepoApi} from './plugins/gr-repo-api/gr-repo-api.js';
import {GrSettingsApi} from './plugins/gr-settings-api/gr-settings-api.js';
import {GrStylesApi} from './plugins/gr-styles-api/gr-styles-api.js';
-import {pluginLoader, PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader, PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context.js';
import {getPluginNameFromUrl, getRestAPI, PLUGIN_LOADING_TIMEOUT_MS, PRELOADED_PROTOCOL, send} from './shared/gr-js-api-interface/gr-api-utils.js';
import {getBaseUrl} from '../utils/url-util.js';
@@ -138,7 +138,7 @@
window.Gerrit.getRootElement = getRootElement;
window.Gerrit.Auth = appContext.authService;
- window.Gerrit._pluginLoader = pluginLoader;
+ window.Gerrit._pluginLoader = getPluginLoader();
// TODO: should define as a getter
window.Gerrit._endpoints = getPluginEndpoints();
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
index c3552cb..9a8f75e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
const pluginApi = _testOnly_initGerritPluginApi();
@@ -29,7 +29,7 @@
let plugin;
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
adminApi = plugin.admin();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 058f022..72cdc46 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -20,7 +20,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-endpoint-decorator_html.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
const INIT_PROPERTIES_TIMEOUT_MS = 10000;
@@ -162,7 +162,7 @@
this._endpointCallBack = this._initModule.bind(this);
getPluginEndpoints().onNewEndpoint(this.name, this._endpointCallBack);
if (this.name) {
- pluginLoader.awaitPluginsLoaded()
+ getPluginLoader().awaitPluginsLoaded()
.then(() => getPluginEndpoints().getAndImportPlugins(this.name))
.then(() =>
getPluginEndpoints()
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
index d7d53cf..c48258d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
@@ -21,7 +21,7 @@
import '../gr-endpoint-slot/gr-endpoint-slot.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {resetPlugins} from '../../../test/test-utils.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
@@ -71,7 +71,7 @@
replacementHook = plugin.registerCustomComponent(
'second', 'other-module', {replace: true});
// Mimic all plugins loaded.
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
flush(done);
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 5d29a4c..0e51116 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -21,7 +21,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-external-style_html.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
/** @extends PolymerElement */
class GrExternalStyle extends GestureEventListeners(
@@ -75,7 +75,8 @@
/** @override */
ready() {
super.ready();
- pluginLoader.awaitPluginsLoaded().then(() => this._importAndApply());
+ getPluginLoader().awaitPluginsLoaded()
+ .then(() => this._importAndApply());
}
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
index 6e57994..ad30f48 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
@@ -18,7 +18,7 @@
import '../../../test/common-test-setup-karma.js';
import {resetPlugins} from '../../../test/test-utils.js';
import './gr-external-style.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
@@ -68,7 +68,7 @@
setup(() => {
sinon.stub(getPluginEndpoints(), 'importUrl')
.callsFake( url => Promise.resolve());
- sinon.stub(pluginLoader, 'awaitPluginsLoaded')
+ sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
.returns(Promise.resolve());
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 46e37e1..6c10c7c 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -18,7 +18,7 @@
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
/** @extends PolymerElement */
class GrPluginHost extends GestureEventListeners(
@@ -42,7 +42,7 @@
plugins && plugins.js_resource_paths || [], htmlPlugins
);
const shouldLoadTheme = config.default_theme &&
- !pluginLoader.isPluginPreloaded('preloaded:gerrit-theme');
+ !getPluginLoader().isPluginPreloaded('preloaded:gerrit-theme');
const themeToLoad =
shouldLoadTheme ? [config.default_theme] : [];
@@ -57,7 +57,7 @@
pluginOpts[config.default_theme] = {sync: true};
}
- pluginLoader.loadPlugins(pluginsPending, pluginOpts);
+ getPluginLoader().loadPlugins(pluginsPending, pluginOpts);
}
/**
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js
index 83e0a84..7a99dc4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import './gr-plugin-host.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
const basicFixture = fixtureFromElement('gr-plugin-host');
@@ -31,21 +31,21 @@
});
test('load plugins should be called', () => {
- sinon.stub(pluginLoader, 'loadPlugins');
+ sinon.stub(getPluginLoader(), 'loadPlugins');
element.config = {
plugin: {
html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
js_resource_paths: ['plugins/42'],
},
};
- assert.isTrue(pluginLoader.loadPlugins.calledOnce);
- assert.isTrue(pluginLoader.loadPlugins.calledWith([
+ assert.isTrue(getPluginLoader().loadPlugins.calledOnce);
+ assert.isTrue(getPluginLoader().loadPlugins.calledWith([
'plugins/42', 'plugins/foo/bar', 'plugins/baz',
], {}));
});
test('theme plugins should be loaded if enabled', () => {
- sinon.stub(pluginLoader, 'loadPlugins');
+ sinon.stub(getPluginLoader(), 'loadPlugins');
element.config = {
default_theme: 'gerrit-theme.html',
plugin: {
@@ -53,23 +53,23 @@
js_resource_paths: ['plugins/42'],
},
};
- assert.isTrue(pluginLoader.loadPlugins.calledOnce);
- assert.isTrue(pluginLoader.loadPlugins.calledWith([
+ assert.isTrue(getPluginLoader().loadPlugins.calledOnce);
+ assert.isTrue(getPluginLoader().loadPlugins.calledWith([
'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
], {'gerrit-theme.html': {sync: true}}));
});
test('skip theme if preloaded', () => {
- sinon.stub(pluginLoader, 'isPluginPreloaded')
+ sinon.stub(getPluginLoader(), 'isPluginPreloaded')
.withArgs('preloaded:gerrit-theme')
.returns(true);
- sinon.stub(pluginLoader, 'loadPlugins');
+ sinon.stub(getPluginLoader(), 'loadPlugins');
element.config = {
default_theme: '/oof',
plugin: {},
};
- assert.isTrue(pluginLoader.loadPlugins.calledOnce);
- assert.isTrue(pluginLoader.loadPlugins.calledWith([], {}));
+ assert.isTrue(getPluginLoader().loadPlugins.calledOnce);
+ assert.isTrue(getPluginLoader().loadPlugins.calledWith([], {}));
});
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js
index c7f1b06..9e24fda 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
@@ -35,7 +35,7 @@
let plugin;
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
repoApi = plugin.project();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js
index 82d58fe..893f3e4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
@@ -37,7 +37,7 @@
let plugin;
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
settingsApi = plugin.settings();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js
index 94d8b82..c41b551 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js
@@ -18,7 +18,7 @@
import '../../../test/common-test-setup-karma.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
@@ -41,7 +41,7 @@
let plugin;
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
stylesApi = plugin.styles();
});
@@ -68,7 +68,7 @@
let plugin;
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
stylesApi = plugin.styles();
displayInlineStyle = stylesApi.css('display: inline');
displayNoneStyle = stylesApi.css('display: none');
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js
index 8a70303..787ab0b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
@@ -56,7 +56,7 @@
/** @override */
ready() { customHeader = this; },
});
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
});
test('sets logo and title', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index aa2edbf..9122a87 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -136,7 +136,11 @@
}
_hasAttention(config, highlight, account, change, force) {
- if (force) return true;
+ return force || this._hasUnforcedAttention(config, highlight, account,
+ change);
+ }
+
+ _hasUnforcedAttention(config, highlight, account, change) {
return this._isAttentionSetEnabled(config, highlight, account, change)
&& change.attention_set
&& change.attention_set.hasOwnProperty(account._account_id);
@@ -196,6 +200,12 @@
targetIsSelf: targetId === selfId,
};
}
+
+ _computeAttentionIconTtitle(config, highlight, account, change) {
+ return this._hasUnforcedAttention(config, highlight, account, change)
+ ? 'Click to remove the user from the attention set'
+ : 'Disabled. Use "Modify" to make changes.';
+ }
}
customElements.define(GrAccountLabel.is, GrAccountLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
index a721734..e3c0e14 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
@@ -106,8 +106,9 @@
link=""
aria-label="Remove user from attention set"
on-click="_handleRemoveAttentionClick"
- has-tooltip=""
- title="Click to remove the user from the attention set"
+ disabled="[[!_hasUnforcedAttention(_config, highlightAttention, account, change)]]"
+ has-tooltip="[[_hasUnforcedAttention(_config, highlightAttention, account, change)]]"
+ title="[[_computeAttentionIconTtitle(_config, highlightAttention, account, change)]]"
><iron-icon class="attention" icon="gr-icons:attention"></iron-icon>
</gr-button>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
deleted file mode 100644
index 0d30179..0000000
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../styles/shared-styles.js';
-import '../gr-js-api-interface/gr-js-api-interface.js';
-import '../gr-rest-api-interface/gr-rest-api-interface.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-avatar_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
-
-/**
- * @extends PolymerElement
- */
-class GrAvatar extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-avatar'; }
-
- static get properties() {
- return {
- account: {
- type: Object,
- observer: '_accountChanged',
- },
- imageSize: {
- type: Number,
- value: 16,
- },
- _hasAvatars: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- Promise.all([
- this._getConfig(),
- pluginLoader.awaitPluginsLoaded(),
- ]).then(([cfg]) => {
- this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
-
- this._updateAvatarURL();
- });
- }
-
- _getConfig() {
- return this.$.restAPI.getConfig();
- }
-
- _accountChanged(account) {
- this._updateAvatarURL();
- }
-
- _updateAvatarURL() {
- if (!this._hasAvatars || !this.account) {
- this.hidden = true;
- return;
- }
- this.hidden = false;
-
- const url = this._buildAvatarURL(this.account);
- if (url) {
- this.style.backgroundImage = 'url("' + url + '")';
- }
- }
-
- _getAccounts(account) {
- return account._account_id || account.email || account.username ||
- account.name;
- }
-
- _buildAvatarURL(account) {
- if (!account) { return ''; }
- const avatars = account.avatars || [];
- for (let i = 0; i < avatars.length; i++) {
- if (avatars[i].height === this.imageSize) {
- return avatars[i].url;
- }
- }
- return getBaseUrl() + '/accounts/' +
- encodeURIComponent(this._getAccounts(account)) +
- '/avatar?s=' + this.imageSize;
- }
-}
-
-customElements.define(GrAvatar.is, GrAvatar);
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
new file mode 100644
index 0000000..45bac9f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../styles/shared-styles';
+import '../gr-js-api-interface/gr-js-api-interface';
+import '../gr-rest-api-interface/gr-rest-api-interface';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-avatar_html';
+import {getBaseUrl} from '../../../utils/url-util';
+import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
+import {customElement, property} from '@polymer/decorators';
+import {AccountInfo} from '../../../types/common';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+
+export interface GrAvatar {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+@customElement('gr-avatar')
+export class GrAvatar extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ @property({type: Object, observer: '_accountChanged'})
+ account?: AccountInfo;
+
+ @property({type: Number})
+ imageSize = 16;
+
+ @property({type: Boolean})
+ _hasAvatars = false;
+
+ /** @override */
+ attached() {
+ super.attached();
+ Promise.all([
+ this._getConfig(),
+ getPluginLoader().awaitPluginsLoaded(),
+ ]).then(([cfg]) => {
+ this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+
+ this._updateAvatarURL();
+ });
+ }
+
+ _getConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _accountChanged() {
+ this._updateAvatarURL();
+ }
+
+ _updateAvatarURL() {
+ if (!this._hasAvatars || !this.account) {
+ this.hidden = true;
+ return;
+ }
+ this.hidden = false;
+
+ const url = this._buildAvatarURL(this.account);
+ if (url) {
+ this.style.backgroundImage = 'url("' + url + '")';
+ }
+ }
+
+ _getAccounts(account: AccountInfo) {
+ return (
+ account._account_id || account.email || account.username || account.name
+ );
+ }
+
+ _buildAvatarURL(account: AccountInfo) {
+ if (!account) {
+ return '';
+ }
+ const avatars = account.avatars || [];
+ for (let i = 0; i < avatars.length; i++) {
+ if (avatars[i].height === this.imageSize) {
+ return avatars[i].url;
+ }
+ }
+ const accountID = this._getAccounts(account);
+ if (!accountID) {
+ return '';
+ }
+ return (
+ `${getBaseUrl()}/accounts/` +
+ encodeURIComponent(`${this._getAccounts(account)}`) +
+ `/avatar?s=${this.imageSize}`
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-avatar': GrAvatar;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
index 5e43f90..261d59c 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import './gr-avatar.js';
-import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
+import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
const basicFixture = fixtureFromElement('gr-avatar');
@@ -101,11 +101,11 @@
assert.strictEqual(element.style.backgroundImage, '');
// Emulate plugins loaded.
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
return Promise.all([
element.$.restAPI.getConfig(),
- pluginLoader.awaitPluginsLoaded(),
+ getPluginLoader().awaitPluginsLoaded(),
]).then(() => {
assert.isFalse(element.hasAttribute('hidden'));
@@ -131,11 +131,11 @@
assert.isFalse(element.hasAttribute('hidden'));
// Emulate plugins loaded.
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
return Promise.all([
element.$.restAPI.getConfig(),
- pluginLoader.awaitPluginsLoaded(),
+ getPluginLoader().awaitPluginsLoaded(),
]).then(() => {
assert.isTrue(element.hasAttribute('hidden'));
@@ -164,11 +164,11 @@
_account_id: 123,
};
// Emulate plugins loaded.
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
return Promise.all([
element.$.restAPI.getConfig(),
- pluginLoader.awaitPluginsLoaded(),
+ getPluginLoader().awaitPluginsLoaded(),
]).then(() => {
assert.isTrue(element.hasAttribute('hidden'));
});
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index 7474f7b..0aa077e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -22,6 +22,7 @@
import {htmlTemplate} from './gr-change-star_html';
import {customElement, property} from '@polymer/decorators';
import {ChangeInfo} from '../../../types/common';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
declare global {
interface HTMLElementTagNameMap {
@@ -30,8 +31,8 @@
}
@customElement('gr-change-star')
-export class GrChangeStar extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)
+export class GrChangeStar extends KeyboardShortcutMixin(
+ GestureEventListeners(LegacyElementMixin(PolymerElement))
) {
static get template() {
return htmlTemplate;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
index c7930c0..6c0f6f7 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
@@ -39,7 +39,9 @@
</style>
<button
role="checkbox"
- aria-label="[[_computeAriaLabel(change.starred)]]]"
+ title="[[createTitle(Shortcut.TOGGLE_CHANGE_STAR,
+ ShortcutSection.ACTIONS)]]"
+ aria-label="[[_computeAriaLabel(change.starred)]]"
on-click="toggleStar"
>
<iron-icon
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 1ee81ac..1ffdf7a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -18,7 +18,7 @@
import '../../../test/common-test-setup-karma.js';
import '../../change/gr-change-actions/gr-change-actions.js';
import {resetPlugins} from '../../../test/test-utils.js';
-import {pluginLoader} from './gr-plugin-loader.js';
+import {getPluginLoader} from './gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
const basicFixture = fixtureFromElement('gr-change-actions');
@@ -44,7 +44,7 @@
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
// Mimic all plugins loaded.
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
changeActions = plugin.changeActions();
element = basicFixture.instantiate();
});
@@ -72,7 +72,7 @@
'http://test.com/plugins/testplugin/static/test.js');
changeActions = plugin.changeActions();
// Mimic all plugins loaded.
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
});
teardown(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index e5e6069..7f0f5b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -19,7 +19,7 @@
* This defines the Gerrit instance. All methods directly attached to Gerrit
* should be defined or linked here.
*/
-import {pluginLoader, PluginOptionMap} from './gr-plugin-loader';
+import {getPluginLoader, PluginOptionMap} from './gr-plugin-loader';
import {getRestAPI, send} from './gr-api-utils';
import {appContext} from '../../../services/app-context';
import {PluginApi} from '../../plugins/gr-plugin-types';
@@ -81,7 +81,7 @@
// Preloaded plugins should be installed after Gerrit.install() is set,
// since plugin preloader substitutes Gerrit.install() temporarily.
// (Gerrit.install() is set in initGerritPluginsMethods)
- pluginLoader.installPreloadedPlugins();
+ getPluginLoader().installPreloadedPlugins();
}
export function _testOnly_initGerritPluginApi(): GerritGlobal {
@@ -136,7 +136,7 @@
};
globalGerritObj.install = (callback, opt_version, opt_src) => {
- pluginLoader.install(callback, opt_version, opt_src);
+ getPluginLoader().install(callback, opt_version, opt_src);
};
globalGerritObj.getLoggedIn = () => {
@@ -181,29 +181,29 @@
};
globalGerritObj.awaitPluginsLoaded = () => {
- return pluginLoader.awaitPluginsLoaded();
+ return getPluginLoader().awaitPluginsLoaded();
};
// TODO(taoalpha): consider removing these proxy methods
- // and using pluginLoader directly
+ // and using getPluginLoader() directly
globalGerritObj._loadPlugins = (plugins, opt_option) => {
- pluginLoader.loadPlugins(plugins, opt_option);
+ getPluginLoader().loadPlugins(plugins, opt_option);
};
globalGerritObj._arePluginsLoaded = () => {
- return pluginLoader.arePluginsLoaded();
+ return getPluginLoader().arePluginsLoaded();
};
globalGerritObj._isPluginPreloaded = url => {
- return pluginLoader.isPluginPreloaded(url);
+ return getPluginLoader().isPluginPreloaded(url);
};
globalGerritObj._isPluginEnabled = pathOrUrl => {
- return pluginLoader.isPluginEnabled(pathOrUrl);
+ return getPluginLoader().isPluginEnabled(pathOrUrl);
};
globalGerritObj._isPluginLoaded = pathOrUrl => {
- return pluginLoader.isPluginLoaded(pathOrUrl);
+ return getPluginLoader().isPluginLoaded(pathOrUrl);
};
const eventEmitter = appContext.eventEmitter;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
index e5b32f7..39e3d1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import './gr-js-api-interface.js';
-import {pluginLoader} from './gr-plugin-loader.js';
+import {getPluginLoader} from './gr-plugin-loader.js';
import {resetPlugins} from '../../../test/test-utils.js';
import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
@@ -52,10 +52,10 @@
});
suite('proxy methods', () => {
- test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
+ test('Gerrit._isPluginEnabled proxy to getPluginLoader()', () => {
const stubFn = sinon.stub();
sinon.stub(
- pluginLoader,
+ getPluginLoader(),
'isPluginEnabled')
.callsFake((...args) => stubFn(...args)
);
@@ -63,20 +63,20 @@
assert.isTrue(stubFn.calledWith('test_plugin'));
});
- test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
+ test('Gerrit._isPluginLoaded proxy to getPluginLoader()', () => {
const stubFn = sinon.stub();
sinon.stub(
- pluginLoader,
+ getPluginLoader(),
'isPluginLoaded')
.callsFake((...args) => stubFn(...args));
pluginApi._isPluginLoaded('test_plugin');
assert.isTrue(stubFn.calledWith('test_plugin'));
});
- test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
+ test('Gerrit._isPluginPreloaded proxy to getPluginLoader()', () => {
const stubFn = sinon.stub();
sinon.stub(
- pluginLoader,
+ getPluginLoader(),
'isPluginPreloaded')
.callsFake((...args) => stubFn(...args));
pluginApi._isPluginPreloaded('test_plugin');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 4973594..d2bb8fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -17,7 +17,7 @@
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {pluginLoader} from './gr-plugin-loader';
+import {getPluginLoader} from './gr-plugin-loader';
import {patchNumEquals} from '../../../utils/patch-set-util';
import {customElement} from '@polymer/decorators';
import {ChangeInfo, RevisionInfo} from '../../../types/common';
@@ -42,31 +42,36 @@
extends GestureEventListeners(LegacyElementMixin(PolymerElement))
implements JsApiService {
handleEvent(type: EventType, detail: any) {
- pluginLoader.awaitPluginsLoaded().then(() => {
- switch (type) {
- case EventType.HISTORY:
- this._handleHistory(detail);
- break;
- case EventType.SHOW_CHANGE:
- this._handleShowChange(detail);
- break;
- case EventType.COMMENT:
- this._handleComment(detail);
- break;
- case EventType.LABEL_CHANGE:
- this._handleLabelChange(detail);
- break;
- case EventType.SHOW_REVISION_ACTIONS:
- this._handleShowRevisionActions(detail);
- break;
- case EventType.HIGHLIGHTJS_LOADED:
- this._handleHighlightjsLoaded(detail);
- break;
- default:
- console.warn('handleEvent called with unsupported event type:', type);
- break;
- }
- });
+ getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(() => {
+ switch (type) {
+ case EventType.HISTORY:
+ this._handleHistory(detail);
+ break;
+ case EventType.SHOW_CHANGE:
+ this._handleShowChange(detail);
+ break;
+ case EventType.COMMENT:
+ this._handleComment(detail);
+ break;
+ case EventType.LABEL_CHANGE:
+ this._handleLabelChange(detail);
+ break;
+ case EventType.SHOW_REVISION_ACTIONS:
+ this._handleShowRevisionActions(detail);
+ break;
+ case EventType.HIGHLIGHTJS_LOADED:
+ this._handleHighlightjsLoaded(detail);
+ break;
+ default:
+ console.warn(
+ 'handleEvent called with unsupported event type:',
+ type
+ );
+ break;
+ }
+ });
}
addElement(key: TargetElement, el: HTMLElement) {
@@ -274,13 +279,15 @@
* will resolve to null.
*/
getCoverageAnnotationApi(): Promise<CoverageProvider | undefined> {
- return pluginLoader.awaitPluginsLoaded().then(
- () =>
- this._getEventCallbacks(EventType.ANNOTATE_DIFF).find(cb => {
- const annotationApi = (cb as unknown) as GrAnnotationActionsInterface;
- return annotationApi.getCoverageProvider();
- }) as CoverageProvider | undefined
- );
+ return getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(
+ () =>
+ this._getEventCallbacks(EventType.ANNOTATE_DIFF).find(cb => {
+ const annotationApi = (cb as unknown) as GrAnnotationActionsInterface;
+ return annotationApi.getCoverageProvider();
+ }) as CoverageProvider | undefined
+ );
}
getAdminMenuLinks() {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 272a13b..e856788 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -22,7 +22,7 @@
import {EventType} from '../../plugins/gr-plugin-types.js';
import {GrPluginActionContext} from './gr-plugin-action-context.js';
import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
-import {pluginLoader} from './gr-plugin-loader.js';
+import {getPluginLoader} from './gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
import {stubBaseUrl} from '../../../test/test-utils.js';
@@ -60,7 +60,7 @@
errorStub = sinon.stub(console, 'error');
pluginApi.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- pluginLoader.loadPlugins([]);
+ getPluginLoader().loadPlugins([]);
});
teardown(() => {
@@ -234,7 +234,7 @@
revisions: {def: {_number: 2}, abc: {_number: 1}},
};
const spy = sinon.spy();
- pluginLoader.loadPlugins(['plugins/test.html']);
+ getPluginLoader().loadPlugins(['plugins/test.html']);
plugin.on(EventType.SHOW_CHANGE, spy);
element.handleEvent(EventType.SHOW_CHANGE,
{change: testChange, patchNum: 1});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 31254ce..1334763 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -444,8 +444,12 @@
}
// TODO(dmfilippov): Convert to service and add to appContext
-export let pluginLoader = new PluginLoader();
+let pluginLoader = new PluginLoader();
export function _testOnly_resetPluginLoader() {
pluginLoader = new PluginLoader();
return pluginLoader;
}
+
+export function getPluginLoader() {
+ return pluginLoader;
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
index 1e6938e..5accc68 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
@@ -540,4 +540,3 @@
});
});
});
-
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index ab73b59..a1703a2 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -613,7 +613,9 @@
getShortcut(shortcutName: Shortcut) {
const bindings = this.bindings.get(shortcutName);
return bindings
- ? bindings.map(binding => this.describeBinding(binding)).join(', ')
+ ? bindings
+ .map(binding => this.describeBinding(binding).join('+'))
+ .join(',')
: '';
}
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
index 534ead4..defc7dc 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
@@ -79,7 +79,16 @@
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
- assert.equal(mgr.getShortcut(NEXT_FILE), '], }, →');
+ assert.equal(mgr.getShortcut(NEXT_FILE), '],},→');
+ });
+
+ test('getShortcut with modifiers', () => {
+ const mgr = new ShortcutManager();
+ const NEXT_FILE = Shortcut.NEXT_FILE;
+
+ assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
+ mgr.bindShortcut(NEXT_FILE, 'Shift+a:key');
+ assert.equal(mgr.getShortcut(NEXT_FILE), 'Shift+a');
});
suite('binding descriptions', () => {