Merge "Fix project-watchers visibility checks."
diff --git a/.gitignore b/.gitignore
index 53bc9f6..0bbcaba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,8 @@
/infer-out
/local.properties
/node_modules/
+/polygerrit-ui/node_modules/
+/polygerrit-ui/app/node_modules/
/package-lock.json
/plugins/*
/polygerrit-ui/coverage/
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 41f544d..dd82f27 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -132,6 +132,10 @@
=== settings-screen
This endpoint is situated at the end of the body of the settings screen.
+=== profile
+This endpoint is situated at the top of the Profile section of the settings
+screen below the section description text.
+
=== reply-text
This endpoint wraps the textarea in the reply dialog.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 65275bd..2f144c6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7216,11 +7216,16 @@
Whether the new change should be set to work in progress.
|`base_change` |optional|
A link:#change-id[\{change-id\}] that identifies the base change for a create
-change operation. Mutually exclusive with `base_commit`.
+change operation. +
+Mutually exclusive with `base_commit`. +
+If neither `base_commit` nor `base_change` are set, the target branch tip will
+be used as the parent commit.
|`base_commit` |optional|
A 40-digit hex SHA-1 of the commit which will be the parent commit of the newly
-created change. If set, it must be a merged commit on the destination branch.
-Mutually exclusive with `base_change`.
+created change. If set, it must be a merged commit on the destination branch. +
+Mutually exclusive with `base_change`. +
+If neither `base_commit` nor `base_change` are set, the target branch tip will
+be used as the parent commit.
|`new_branch` |optional, default to `false`|
Allow creating a new branch when set to `true`. Using this option is
only possible for non-merge commits (if the `merge` field is not set).
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 0073ec2..df2c5cb 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -55,6 +55,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -308,6 +309,7 @@
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
+ modules.add(new ProjectQueryBuilderModule());
modules.add(new PluginApiModule());
modules.add(new SearchingChangeCacheImplModule());
modules.add(new InternalAccountDirectoryModule());
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 3114b4c..e050f53 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -15,14 +15,12 @@
package com.google.gerrit.index.project;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.index.FieldDef.exact;
-import static com.google.gerrit.index.FieldDef.fullText;
-import static com.google.gerrit.index.FieldDef.prefix;
import static com.google.gerrit.index.FieldDef.storedOnly;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.RefState;
import com.google.gerrit.index.SchemaUtil;
@@ -38,23 +36,53 @@
.toByteArray(project.getNameKey());
}
- public static final FieldDef<ProjectData, String> NAME =
- exact("name").stored().build(p -> p.getProject().getName());
+ public static final IndexedField<ProjectData, String> NAME_FIELD =
+ IndexedField.<ProjectData>stringBuilder("RepoName")
+ .required()
+ .size(200)
+ .stored()
+ .build(p -> p.getProject().getName());
- public static final FieldDef<ProjectData, String> DESCRIPTION =
- fullText("description").stored().build(p -> p.getProject().getDescription());
+ public static final IndexedField<ProjectData, String>.SearchSpec NAME_SPEC =
+ NAME_FIELD.exact("name");
- public static final FieldDef<ProjectData, String> PARENT_NAME =
- exact("parent_name").build(p -> p.getProject().getParentName());
+ public static final IndexedField<ProjectData, String> DESCRIPTION_FIELD =
+ IndexedField.<ProjectData>stringBuilder("Description")
+ .stored()
+ .build(p -> p.getProject().getDescription());
- public static final FieldDef<ProjectData, Iterable<String>> NAME_PART =
- prefix("name_part").buildRepeatable(p -> SchemaUtil.getNameParts(p.getProject().getName()));
+ public static final IndexedField<ProjectData, String>.SearchSpec DESCRIPTION_SPEC =
+ DESCRIPTION_FIELD.fullText("description");
- public static final FieldDef<ProjectData, String> STATE =
- exact("state").stored().build(p -> p.getProject().getState().name());
+ public static final IndexedField<ProjectData, String> PARENT_NAME_FIELD =
+ IndexedField.<ProjectData>stringBuilder("ParentName")
+ .build(p -> p.getProject().getParentName());
- public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
- exact("ancestor_name").buildRepeatable(ProjectData::getParentNames);
+ public static final IndexedField<ProjectData, String>.SearchSpec PARENT_NAME_SPEC =
+ PARENT_NAME_FIELD.exact("parent_name");
+
+ public static final IndexedField<ProjectData, Iterable<String>> NAME_PART_FIELD =
+ IndexedField.<ProjectData>iterableStringBuilder("NamePart")
+ .size(200)
+ .build(p -> SchemaUtil.getNameParts(p.getProject().getName()));
+
+ public static final IndexedField<ProjectData, Iterable<String>>.SearchSpec NAME_PART_SPEC =
+ NAME_PART_FIELD.prefix("name_part");
+
+ public static final IndexedField<ProjectData, String> STATE_FIELD =
+ IndexedField.<ProjectData>stringBuilder("State")
+ .stored()
+ .build(p -> p.getProject().getState().name());
+
+ public static final IndexedField<ProjectData, String>.SearchSpec STATE_SPEC =
+ STATE_FIELD.exact("state");
+
+ public static final IndexedField<ProjectData, Iterable<String>> ANCESTOR_NAME_FIELD =
+ IndexedField.<ProjectData>iterableStringBuilder("AncestorName")
+ .build(ProjectData::getParentNames);
+
+ public static final IndexedField<ProjectData, Iterable<String>>.SearchSpec ANCESTOR_NAME_SPEC =
+ ANCESTOR_NAME_FIELD.exact("ancestor_name");
/**
* All values of all refs that were used in the course of indexing this document. This covers
diff --git a/java/com/google/gerrit/index/project/ProjectIndex.java b/java/com/google/gerrit/index/project/ProjectIndex.java
index 8687544..0aa7393 100644
--- a/java/com/google/gerrit/index/project/ProjectIndex.java
+++ b/java/com/google/gerrit/index/project/ProjectIndex.java
@@ -31,7 +31,7 @@
@Override
default Predicate<ProjectData> keyPredicate(Project.NameKey nameKey) {
- return new ProjectPredicate(ProjectField.NAME, nameKey.get());
+ return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
Function<ProjectData, Project.NameKey> ENTITY_TO_KEY = (p) -> p.getProject().getNameKey();
diff --git a/java/com/google/gerrit/index/project/ProjectPredicate.java b/java/com/google/gerrit/index/project/ProjectPredicate.java
index 11875ef..0eaf2b6 100644
--- a/java/com/google/gerrit/index/project/ProjectPredicate.java
+++ b/java/com/google/gerrit/index/project/ProjectPredicate.java
@@ -14,12 +14,12 @@
package com.google.gerrit.index.project;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.IndexPredicate;
/** Predicate that is mapped to a field in the project index. */
public class ProjectPredicate extends IndexPredicate<ProjectData> {
- public ProjectPredicate(FieldDef<ProjectData, ?> def, String value) {
+ public ProjectPredicate(SchemaField<ProjectData, ?> def, String value) {
super(def, value);
}
}
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 0619566..05c23e1 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -16,6 +16,8 @@
import static com.google.gerrit.index.SchemaUtil.schema;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaDefinitions;
@@ -31,14 +33,26 @@
static final Schema<ProjectData> V1 =
schema(
/* version= */ 1,
- ProjectField.NAME,
- ProjectField.DESCRIPTION,
- ProjectField.PARENT_NAME,
- ProjectField.NAME_PART,
- ProjectField.ANCESTOR_NAME);
+ ImmutableList.of(
+ ProjectField.NAME_FIELD,
+ ProjectField.DESCRIPTION_FIELD,
+ ProjectField.PARENT_NAME_FIELD,
+ ProjectField.NAME_PART_FIELD,
+ ProjectField.ANCESTOR_NAME_FIELD),
+ ImmutableList.<IndexedField<ProjectData, ?>.SearchSpec>of(
+ ProjectField.NAME_SPEC,
+ ProjectField.DESCRIPTION_SPEC,
+ ProjectField.PARENT_NAME_SPEC,
+ ProjectField.NAME_PART_SPEC,
+ ProjectField.ANCESTOR_NAME_SPEC));
@Deprecated
- static final Schema<ProjectData> V2 = schema(V1, ProjectField.STATE, ProjectField.REF_STATE);
+ static final Schema<ProjectData> V2 =
+ schema(
+ V1,
+ ImmutableList.of(ProjectField.REF_STATE),
+ ImmutableList.<IndexedField<ProjectData, ?>>of(ProjectField.STATE_FIELD),
+ ImmutableList.<IndexedField<ProjectData, ?>.SearchSpec>of(ProjectField.STATE_SPEC));
// Bump Lucene version requires reindexing
@Deprecated static final Schema<ProjectData> V3 = schema(V2);
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index 23ae312..fda961d 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -134,7 +134,7 @@
cmp = a.estimateCost() - b.estimateCost();
}
- if (cmp == 0 && a instanceof DataSource && b instanceof DataSource) {
+ if (cmp == 0 && a instanceof DataSource) {
DataSource<?> as = (DataSource<?>) a;
DataSource<?> bs = (DataSource<?>) b;
cmp = as.getCardinality() - bs.getCardinality();
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index fae854e..911d91f 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -15,7 +15,7 @@
package com.google.gerrit.lucene;
import static com.google.common.collect.Iterables.getOnlyElement;
-import static com.google.gerrit.index.project.ProjectField.NAME;
+import static com.google.gerrit.index.project.ProjectField.NAME_SPEC;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
@@ -58,14 +58,14 @@
implements ProjectIndex {
private static final String PROJECTS = "projects";
- private static final String NAME_SORT_FIELD = sortFieldName(NAME);
+ private static final String NAME_SORT_FIELD = sortFieldName(NAME_SPEC);
private static Term idTerm(ProjectData projectState) {
return idTerm(projectState.getProject().getNameKey());
}
private static Term idTerm(Project.NameKey nameKey) {
- return QueryBuilder.stringTerm(NAME.getName(), nameKey.get());
+ return QueryBuilder.stringTerm(NAME_SPEC.getName(), nameKey.get());
}
private final GerritIndexWriterConfig indexWriterConfig;
@@ -110,7 +110,7 @@
void add(Document doc, Values<ProjectData> values) {
// Add separate DocValues field for the field that is needed for sorting.
SchemaField<ProjectData, ?> f = values.getField();
- if (f == NAME) {
+ if (f == NAME_SPEC) {
String value = (String) getOnlyElement(values.getValues());
doc.add(new SortedDocValuesField(NAME_SORT_FIELD, new BytesRef(value)));
}
@@ -156,7 +156,7 @@
@Nullable
@Override
protected ProjectData fromDocument(Document doc) {
- Project.NameKey nameKey = Project.nameKey(doc.getField(NAME.getName()).stringValue());
+ Project.NameKey nameKey = Project.nameKey(doc.getField(NAME_SPEC.getName()).stringValue());
return projectCache.get().get(nameKey).map(ProjectState::toProjectData).orElse(null);
}
}
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 75891fe..0342fe5 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -64,6 +64,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -446,6 +447,7 @@
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
+ modules.add(new ProjectQueryBuilderModule());
modules.add(new PluginApiModule());
modules.add(new SearchingChangeCacheImplModule(replica));
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 8d5fea4..d6ea294 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -202,7 +202,6 @@
private ExternalIdNotes externalIdNotes;
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
@@ -228,7 +227,6 @@
}
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
diff --git a/java/com/google/gerrit/server/account/GroupCache.java b/java/com/google/gerrit/server/account/GroupCache.java
index 1e28d7d..46c730c 100644
--- a/java/com/google/gerrit/server/account/GroupCache.java
+++ b/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,11 +14,15 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.exceptions.StorageException;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
/** Tracks group objects in memory for efficient access. */
public interface GroupCache {
@@ -62,6 +66,22 @@
Map<AccountGroup.UUID, InternalGroup> get(Collection<AccountGroup.UUID> groupUuids);
/**
+ * Returns an {@code InternalGroup} instance for the given {@code AccountGroup.UUID} at the given
+ * {@code metaId} of {@link com.google.gerrit.entities.RefNames#refsGroups} ref.
+ *
+ * <p>The caller is responsible to ensure the presence of {@code metaId} and the corresponding
+ * meta ref.
+ *
+ * @param groupUuid the UUID of the internal group
+ * @param metaId the sha1 of commit in {@link com.google.gerrit.entities.RefNames#refsGroups} ref.
+ * @return the internal group at specific sha1 {@code metaId}
+ * @throws StorageException if no internal group with this UUID exists on this server at the
+ * specific sha1, or if an error occurred during lookup.
+ */
+ @UsedAt(Project.GOOGLE)
+ InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId) throws StorageException;
+
+ /**
* Removes the association of the given ID with a group.
*
* <p>The next call to {@link #get(AccountGroup.Id)} won't provide a cached value.
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 2d947ba..6f4fce9 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -27,6 +27,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache;
@@ -122,15 +123,19 @@
private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
private final LoadingCache<String, Optional<InternalGroup>> byName;
private final LoadingCache<String, Optional<InternalGroup>> byUUID;
+ private final LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache;
@Inject
GroupCacheImpl(
@Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
@Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
- @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID) {
+ @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
+ @Named(BYUUID_NAME_PERSISTED)
+ LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
+ this.persistedByUuidCache = persistedByUuidCache;
}
@Override
@@ -185,6 +190,21 @@
}
@Override
+ public InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId)
+ throws StorageException {
+ Cache.GroupKeyProto key =
+ Cache.GroupKeyProto.newBuilder()
+ .setUuid(groupUuid.get())
+ .setRevision(ObjectIdConverter.create().toByteString(metaId))
+ .build();
+ try {
+ return persistedByUuidCache.get(key);
+ } catch (ExecutionException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
public void evict(AccountGroup.Id groupId) {
if (groupId != null) {
logger.atFine().log("Evict group %s by ID", groupId.get());
diff --git a/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java b/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java
new file mode 100644
index 0000000..8ed1175
--- /dev/null
+++ b/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2023 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.api.projects;
+
+import com.google.gerrit.server.query.project.ProjectQueryBuilder;
+import com.google.gerrit.server.query.project.ProjectQueryBuilderImpl;
+import com.google.inject.AbstractModule;
+
+public class ProjectQueryBuilderModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(ProjectQueryBuilder.class).to(ProjectQueryBuilderImpl.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index d26af7b..912d202 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -146,7 +146,7 @@
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
- static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
+ public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
ImmutableSet.of(
ALL_COMMITS,
ALL_REVISIONS,
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 06e41ff..5555ba6 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -48,6 +48,7 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.DeleteVoteControl;
+import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -73,11 +74,16 @@
private final PermissionBackend permissionBackend;
private final DeleteVoteControl deleteVoteControl;
+ private final RemoveReviewerControl removeReviewerControl;
@Inject
- LabelsJson(PermissionBackend permissionBackend, DeleteVoteControl deleteVoteControl) {
+ LabelsJson(
+ PermissionBackend permissionBackend,
+ DeleteVoteControl deleteVoteControl,
+ RemoveReviewerControl removeReviewerControl) {
this.permissionBackend = permissionBackend;
this.deleteVoteControl = deleteVoteControl;
+ this.removeReviewerControl = removeReviewerControl;
}
/**
@@ -161,8 +167,9 @@
if (!labelType.isPresent()) {
continue;
}
- if (!deleteVoteControl.testDeleteVotePermissions(
- user, cd.notes(), approval, labelType.get())) {
+ if (!(deleteVoteControl.testDeleteVotePermissions(user, cd, approval, labelType.get())
+ || removeReviewerControl.testRemoveReviewer(
+ cd, user, approval.accountId(), approval.value()))) {
continue;
}
if (!res.containsKey(approval.label())) {
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index c0c934b..87d8db1 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -115,7 +115,6 @@
private final RetryHelper retryHelper;
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -150,7 +149,6 @@
}
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -185,7 +183,6 @@
Optional.of(currentUser));
}
- @SuppressWarnings("BindingAnnotationWithoutInject")
private GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 213094e..352d376 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -116,9 +116,9 @@
*/
public static Set<String> projectFields(QueryOptions opts) {
Set<String> fs = opts.fields();
- return fs.contains(ProjectField.NAME.getName())
+ return fs.contains(ProjectField.NAME_SPEC.getName())
? fs
- : Sets.union(fs, ImmutableSet.of(ProjectField.NAME.getName()));
+ : Sets.union(fs, ImmutableSet.of(ProjectField.NAME_SPEC.getName()));
}
private IndexUtils() {
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index 9c44c00..9f6bb31 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -40,7 +40,7 @@
*/
public class StalenessChecker {
private static final ImmutableSet<String> FIELDS =
- ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+ ImmutableSet.of(ProjectField.NAME_SPEC.getName(), ProjectField.REF_STATE.getName());
private final ProjectCache projectCache;
private final ProjectIndexCollection indexes;
diff --git a/java/com/google/gerrit/server/project/DeleteVoteControl.java b/java/com/google/gerrit/server/project/DeleteVoteControl.java
index 93c0451..3f3f88a 100644
--- a/java/com/google/gerrit/server/project/DeleteVoteControl.java
+++ b/java/com/google/gerrit/server/project/DeleteVoteControl.java
@@ -18,7 +18,6 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -26,38 +25,37 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.util.Set;
public class DeleteVoteControl {
private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
@Inject
- public DeleteVoteControl(PermissionBackend permissionBackend) {
+ public DeleteVoteControl(
+ PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory) {
this.permissionBackend = permissionBackend;
- }
-
- public void checkDeleteVotePermissions(
- CurrentUser user, ChangeNotes notes, PatchSetApproval approval, LabelType labelType)
- throws AuthException, PermissionBackendException {
- if (testDeleteVotePermissions(user, notes, approval, labelType)) {
- return;
- }
- throw new AuthException(
- new LabelRemovalPermission.WithValue(labelType, approval.value()).describeForException()
- + " not permitted");
+ this.changeDataFactory = changeDataFactory;
}
public boolean testDeleteVotePermissions(
CurrentUser user, ChangeNotes notes, PatchSetApproval approval, LabelType labelType)
throws PermissionBackendException {
+ return testDeleteVotePermissions(user, changeDataFactory.create(notes), approval, labelType);
+ }
+
+ public boolean testDeleteVotePermissions(
+ CurrentUser user, ChangeData cd, PatchSetApproval approval, LabelType labelType)
+ throws PermissionBackendException {
if (canRemoveReviewerWithoutRemoveLabelPermission(
- notes.getChange(), user, approval.accountId(), approval.value())) {
+ cd.change(), user, approval.accountId(), approval.value())) {
return true;
}
// Test if the user is allowed to remove vote of the given label type and value.
Set<LabelRemovalPermission.WithValue> allowed =
- permissionBackend.user(user).change(notes).testRemoval(labelType);
+ permissionBackend.user(user).change(cd).testRemoval(labelType);
return allowed.contains(new LabelRemovalPermission.WithValue(labelType, approval.value()));
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 7fdd113..0afaa3f 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -76,6 +76,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -300,17 +301,22 @@
@Override
public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
- return Streams.concat(
- Arrays.stream(config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
- .map(AccountGroup::uuid),
- all().stream()
- .map(n -> inMemoryProjectCache.getIfPresent(n))
- .filter(Objects::nonNull)
- .flatMap(p -> p.getAllGroupUUIDs().stream())
- // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
- // against them just in case there is a bug or corner case.
- .filter(id -> id != null && id.get() != null))
- .collect(toSet());
+ Stream<AccountGroup.UUID> configuredRelevantGroups =
+ Arrays.stream(config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
+ .map(AccountGroup::uuid);
+
+ Stream<AccountGroup.UUID> guessedRelevantGroups =
+ inMemoryProjectCache.asMap().values().stream()
+ .filter(Objects::nonNull)
+ .flatMap(p -> p.getAllGroupUUIDs().stream())
+ // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
+ // against them just in case there is a bug or corner case.
+ .filter(id -> id != null && id.get() != null);
+
+ Set<AccountGroup.UUID> relevantGroupUuids =
+ Streams.concat(configuredRelevantGroups, guessedRelevantGroups).collect(toSet());
+ logger.atFine().log("relevant group UUIDs: %s", relevantGroupUuids);
+ return relevantGroupUuids;
}
}
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 1bc309c..3fda87a 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -32,10 +32,12 @@
@Singleton
public class RemoveReviewerControl {
private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
@Inject
- RemoveReviewerControl(PermissionBackend permissionBackend) {
+ RemoveReviewerControl(PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory) {
this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
}
/**
@@ -64,6 +66,20 @@
/** Returns true if the user is allowed to remove this reviewer. */
public boolean testRemoveReviewer(
+ ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
+ throws PermissionBackendException {
+ return testRemoveReviewer(notes, currentUser, approval.accountId(), approval.value());
+ }
+
+ /** Returns true if the user is allowed to remove this reviewer. */
+ public boolean testRemoveReviewer(
+ ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+ throws PermissionBackendException {
+ return testRemoveReviewer(changeDataFactory.create(notes), currentUser, reviewer, value);
+ }
+
+ /** Returns true if the user is allowed to remove this reviewer. */
+ public boolean testRemoveReviewer(
ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
throws PermissionBackendException {
if (canRemoveReviewerWithoutPermissionCheck(
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index e31411c..d5c4a97 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -159,7 +159,7 @@
return AccountPredicates.preferredEmail(email);
}
- throw new QueryParseException("'email' operator is not supported by account index version");
+ throw new QueryParseException("'email' operator is not supported on this gerrit host");
}
@Operator
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index d84f117..714516c 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -700,7 +700,8 @@
if ("mergeable".equalsIgnoreCase(value)) {
if (!args.indexMergeable) {
- throw new QueryParseException("'is:mergeable' operator is not supported by server");
+ throw new QueryParseException(
+ "'is:mergeable' operator is not supported on this gerrit host");
}
return new BooleanPredicate(ChangeField.MERGEABLE_SPEC);
}
@@ -781,7 +782,7 @@
@Operator
public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
if (!args.conflictsPredicateEnabled) {
- throw new QueryParseException("'conflicts:' operator is not supported by server");
+ throw new QueryParseException("'conflicts:' operator is not supported on this gerrit host");
}
List<Change> changes = parseChange(value);
List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
@@ -1651,7 +1652,7 @@
throws QueryParseException {
if (!args.index.getSchema().hasField(field)) {
throw new QueryParseException(
- String.format("'%s' operator is not supported by change index version", operator));
+ String.format("'%s' operator is not supported on this gerrit host", operator));
}
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index 8b4048f..a7b0743 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -25,23 +25,23 @@
/** Utility class to create predicates for project index queries. */
public class ProjectPredicates {
public static Predicate<ProjectData> name(Project.NameKey nameKey) {
- return new ProjectPredicate(ProjectField.NAME, nameKey.get());
+ return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
public static Predicate<ProjectData> parent(Project.NameKey parentNameKey) {
- return new ProjectPredicate(ProjectField.PARENT_NAME, parentNameKey.get());
+ return new ProjectPredicate(ProjectField.PARENT_NAME_SPEC, parentNameKey.get());
}
public static Predicate<ProjectData> inname(String name) {
- return new ProjectPredicate(ProjectField.NAME_PART, name.toLowerCase(Locale.US));
+ return new ProjectPredicate(ProjectField.NAME_PART_SPEC, name.toLowerCase(Locale.US));
}
public static Predicate<ProjectData> description(String description) {
- return new ProjectPredicate(ProjectField.DESCRIPTION, description);
+ return new ProjectPredicate(ProjectField.DESCRIPTION_SPEC, description);
}
public static Predicate<ProjectData> state(ProjectState state) {
- return new ProjectPredicate(ProjectField.STATE, state.name());
+ return new ProjectPredicate(ProjectField.STATE_SPEC, state.name());
}
private ProjectPredicates() {}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index d234546..edb12ec 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2023 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.
@@ -14,93 +14,21 @@
package com.google.gerrit.server.query.project;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.index.project.ProjectData;
-import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.inject.Inject;
import java.util.List;
-/** Parses a query string meant to be applied to project objects. */
-public class ProjectQueryBuilder extends QueryBuilder<ProjectData, ProjectQueryBuilder> {
- public static final String FIELD_LIMIT = "limit";
+/**
+ * Provides methods required for parsing projects queries.
+ *
+ * <p>Internally (at google), this interface has a different implementation, comparing to upstream.
+ */
+public interface ProjectQueryBuilder {
+ String FIELD_LIMIT = "limit";
- private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilder> mydef =
- new QueryBuilder.Definition<>(ProjectQueryBuilder.class);
-
- @Inject
- ProjectQueryBuilder() {
- super(mydef, null);
- }
-
- @Operator
- public Predicate<ProjectData> name(String name) {
- return ProjectPredicates.name(Project.nameKey(name));
- }
-
- @Operator
- public Predicate<ProjectData> parent(String parentName) {
- return ProjectPredicates.parent(Project.nameKey(parentName));
- }
-
- @Operator
- public Predicate<ProjectData> inname(String namePart) {
- if (namePart.isEmpty()) {
- return name(namePart);
- }
- return ProjectPredicates.inname(namePart);
- }
-
- @Operator
- public Predicate<ProjectData> description(String description) throws QueryParseException {
- if (Strings.isNullOrEmpty(description)) {
- throw error("description operator requires a value");
- }
-
- return ProjectPredicates.description(description);
- }
-
- @Operator
- public Predicate<ProjectData> state(String state) throws QueryParseException {
- if (Strings.isNullOrEmpty(state)) {
- throw error("state operator requires a value");
- }
- ProjectState parsedState;
- try {
- parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
- } catch (IllegalArgumentException e) {
- throw error("state operator must be either 'active' or 'read-only'", e);
- }
- if (parsedState == ProjectState.HIDDEN) {
- throw error("state operator must be either 'active' or 'read-only'");
- }
- return ProjectPredicates.state(parsedState);
- }
-
- @Override
- protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
- // Adapt the capacity of this list when adding more default predicates.
- List<Predicate<ProjectData>> preds = Lists.newArrayListWithCapacity(3);
- preds.add(name(query));
- preds.add(inname(query));
- if (!Strings.isNullOrEmpty(query)) {
- preds.add(description(query));
- }
- return Predicate.or(preds);
- }
-
- @Operator
- public Predicate<ProjectData> limit(String query) throws QueryParseException {
- Integer limit = Ints.tryParse(query);
- if (limit == null) {
- throw error("Invalid limit: " + query);
- }
- return new LimitPredicate<>(FIELD_LIMIT, limit);
- }
+ /** See {@link com.google.gerrit.index.query.QueryBuilder#parse(String)}. */
+ Predicate<ProjectData> parse(String query) throws QueryParseException;
+ /** See {@link com.google.gerrit.index.query.QueryBuilder#parse(List<String>)}. */
+ List<Predicate<ProjectData>> parse(List<String> queries) throws QueryParseException;
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
new file mode 100644
index 0000000..f7135982
--- /dev/null
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.project;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.query.LimitPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.inject.Inject;
+import java.util.List;
+
+/** Parses a query string meant to be applied to project objects. */
+public class ProjectQueryBuilderImpl extends QueryBuilder<ProjectData, ProjectQueryBuilderImpl>
+ implements ProjectQueryBuilder {
+ private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilderImpl> mydef =
+ new QueryBuilder.Definition<>(ProjectQueryBuilderImpl.class);
+
+ @Inject
+ ProjectQueryBuilderImpl() {
+ super(mydef, null);
+ }
+
+ @Operator
+ public Predicate<ProjectData> name(String name) {
+ return ProjectPredicates.name(Project.nameKey(name));
+ }
+
+ @Operator
+ public Predicate<ProjectData> parent(String parentName) {
+ return ProjectPredicates.parent(Project.nameKey(parentName));
+ }
+
+ @Operator
+ public Predicate<ProjectData> inname(String namePart) {
+ if (namePart.isEmpty()) {
+ return name(namePart);
+ }
+ return ProjectPredicates.inname(namePart);
+ }
+
+ @Operator
+ public Predicate<ProjectData> description(String description) throws QueryParseException {
+ if (Strings.isNullOrEmpty(description)) {
+ throw error("description operator requires a value");
+ }
+
+ return ProjectPredicates.description(description);
+ }
+
+ @Operator
+ public Predicate<ProjectData> state(String state) throws QueryParseException {
+ if (Strings.isNullOrEmpty(state)) {
+ throw error("state operator requires a value");
+ }
+ ProjectState parsedState;
+ try {
+ parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw error("state operator must be either 'active' or 'read-only'", e);
+ }
+ if (parsedState == ProjectState.HIDDEN) {
+ throw error("state operator must be either 'active' or 'read-only'");
+ }
+ return ProjectPredicates.state(parsedState);
+ }
+
+ @Override
+ protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
+ // Adapt the capacity of this list when adding more default predicates.
+ List<Predicate<ProjectData>> preds = Lists.newArrayListWithCapacity(3);
+ preds.add(name(query));
+ preds.add(inname(query));
+ if (!Strings.isNullOrEmpty(query)) {
+ preds.add(description(query));
+ }
+ return Predicate.or(preds);
+ }
+
+ @Operator
+ public Predicate<ProjectData> limit(String query) throws QueryParseException {
+ Integer limit = Ints.tryParse(query);
+ if (limit == null) {
+ throw error("Invalid limit: " + query);
+ }
+ return new LimitPredicate<>(FIELD_LIMIT, limit);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
index 239b485..0e1a218 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
@@ -20,6 +20,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
@@ -37,9 +38,11 @@
import com.google.gerrit.server.mail.send.DeleteVoteSender;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
+import com.google.gerrit.server.permissions.LabelRemovalPermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.DeleteVoteControl;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.PostUpdateContext;
@@ -76,6 +79,7 @@
private final DeleteVoteSender.Factory deleteVoteSenderFactory;
private final DeleteVoteControl deleteVoteControl;
+ private final RemoveReviewerControl removeReviewerControl;
private final MessageIdGenerator messageIdGenerator;
private final String label;
@@ -98,6 +102,7 @@
DeleteVoteSender.Factory deleteVoteSenderFactory,
DeleteVoteControl deleteVoteControl,
MessageIdGenerator messageIdGenerator,
+ RemoveReviewerControl removeReviewerControl,
@Assisted Project.NameKey projectName,
@Assisted AccountState reviewerToDeleteVoteFor,
@Assisted String label,
@@ -110,6 +115,7 @@
this.voteDeleted = voteDeleted;
this.deleteVoteSenderFactory = deleteVoteSenderFactory;
this.deleteVoteControl = deleteVoteControl;
+ this.removeReviewerControl = removeReviewerControl;
this.messageIdGenerator = messageIdGenerator;
this.projectName = projectName;
@@ -143,8 +149,7 @@
newApprovals.put(a.label(), a.value());
continue;
} else if (enforcePermissions) {
- deleteVoteControl.checkDeleteVotePermissions(
- ctx.getUser(), ctx.getNotes(), a, labelTypes.byLabel(a.labelId()).get());
+ checkPermissions(ctx, labelTypes.byLabel(a.labelId()).get(), a);
}
// Set the approval to 0 if vote is being removed.
newApprovals.put(a.label(), (short) 0);
@@ -205,4 +210,21 @@
user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null,
ctx.getWhen());
}
+
+ private void checkPermissions(ChangeContext ctx, LabelType labelType, PatchSetApproval approval)
+ throws PermissionBackendException, AuthException {
+ boolean permitted =
+ removeReviewerControl.testRemoveReviewer(ctx.getNotes(), ctx.getUser(), approval)
+ || deleteVoteControl.testDeleteVotePermissions(
+ ctx.getUser(), ctx.getNotes(), approval, labelType);
+ if (!permitted) {
+ throw new AuthException(
+ "Delete vote not permitted.",
+ new AuthException(
+ "Both "
+ + new LabelRemovalPermission.WithValue(labelType, approval.value())
+ .describeForException()
+ + " and remove-reviewer are not permitted"));
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index a8ba052..22eb32c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -379,7 +379,8 @@
// Add the review ops.
logger.atFine().log("posting review");
PostReviewOp postReviewOp =
- postReviewOpFactory.create(projectState, revision.getPatchSet().id(), input);
+ postReviewOpFactory.create(
+ projectState, revision.getPatchSet().id(), input, revision.getAccountId());
bu.addOp(revision.getChange().getId(), postReviewOp);
// Adjust the attention set based on the input
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index b7d17f2..29e453b 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -97,7 +97,8 @@
public class PostReviewOp implements BatchUpdateOp {
interface Factory {
- PostReviewOp create(ProjectState projectState, PatchSet.Id psId, ReviewInput in);
+ PostReviewOp create(
+ ProjectState projectState, PatchSet.Id psId, ReviewInput in, Account.Id reviewerId);
}
/**
@@ -192,6 +193,7 @@
private final ProjectState projectState;
private final PatchSet.Id psId;
private final ReviewInput in;
+ private final Account.Id reviewerId;
private final boolean publishPatchSetLevelComment;
private IdentifiedUser user;
@@ -220,7 +222,8 @@
PluginSetContext<OnPostReview> onPostReviews,
@Assisted ProjectState projectState,
@Assisted PatchSet.Id psId,
- @Assisted ReviewInput in) {
+ @Assisted ReviewInput in,
+ @Assisted Account.Id reviewerId) {
this.approvalCopier = approvalCopier;
this.approvalsUtil = approvalsUtil;
this.publishCommentUtil = publishCommentUtil;
@@ -237,6 +240,7 @@
this.projectState = projectState;
this.psId = psId;
this.in = in;
+ this.reviewerId = reviewerId;
}
@Override
@@ -645,10 +649,11 @@
del.add(c);
update.putApproval(normName, (short) 0);
}
- // Only allow voting again if the vote is copied over from a past patch-set, or the
- // values are different.
+ // Only allow voting again the values are different, if the real account differs or if the
+ // vote is copied over from a past patch-set.
} else if (c != null
&& (c.value() != ent.getValue()
+ || !c.realAccountId().equals(reviewerId)
|| (inLabels.containsKey(c.label()) && isApprovalCopiedOver(c, ctx.getNotes())))) {
PatchSetApproval.Builder b =
c.toBuilder()
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
index 59efd06..2f1153e 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectResource;
@@ -59,7 +60,8 @@
throw new AuthException("Authentication required");
}
- if (!Strings.isNullOrEmpty(input.project) && !rsrc.getName().equals(input.project)) {
+ if (!Strings.isNullOrEmpty(input.project)
+ && !rsrc.getName().equals(ProjectUtil.sanitizeProjectName(input.project))) {
throw new BadRequestException("project must match URL");
}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index aadf6d4..b828037 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -191,6 +192,7 @@
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
install(new AuthModule(authConfig));
install(new GerritApiModule());
+ install(new ProjectQueryBuilderModule());
factory(PluginUser.Factory.class);
install(new PluginApiModule());
install(new DefaultPermissionBackendModule());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 92d19bb..215d1e8 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -26,7 +26,6 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
@@ -162,6 +161,7 @@
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.testing.TestChangeETagComputation;
@@ -2268,16 +2268,6 @@
@Test
public void deleteVote() throws Exception {
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
-
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -2321,16 +2311,6 @@
@Test
public void deleteVoteNotifyNone() throws Exception {
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
-
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -2348,16 +2328,6 @@
@Test
public void deleteVoteWithReason() throws Exception {
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
-
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -2379,16 +2349,6 @@
@Test
public void deleteVoteNotifyAccount() throws Exception {
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
-
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -2448,7 +2408,7 @@
.id(r.getChangeId())
.reviewer(admin.id().toString())
.deleteVote(LabelId.CODE_REVIEW));
- assertThat(thrown).hasMessageThat().contains("removeLabel Code-Review=+2 not permitted");
+ assertThat(thrown).hasMessageThat().contains("Delete vote not permitted");
}
@Test
@@ -2466,6 +2426,7 @@
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/heads/*").group(REGISTERED_USERS))
.update();
PushOneCommit.Result r = createChange();
@@ -2495,6 +2456,7 @@
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/heads/*").group(REGISTERED_USERS))
.update();
PushOneCommit.Result r = createChange();
@@ -3097,7 +3059,11 @@
.review(ReviewInput.approve());
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
- createChange();
+ PushOneCommit.Result change = createChange();
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
requestScopeOperations.setApiUser(user.id());
try (AutoCloseable ignored = disableNoteDb()) {
@@ -3112,6 +3078,34 @@
}
@Test
+ public void nonLazyloadQueryOptionsDoNotTouchDatabase() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit.Result change = createChange();
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
+
+ requestScopeOperations.setApiUser(user.id());
+ try (AutoCloseable ignored = disableNoteDb()) {
+ assertThat(
+ gApi.changes()
+ .query()
+ .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
+ .withOptions(EnumSet.complementOf(EnumSet.copyOf(ChangeJson.REQUIRE_LAZY_LOAD)))
+ .get())
+ .hasSize(2);
+ }
+ }
+
+ @Test
public void votable() throws Exception {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
@@ -3391,7 +3385,6 @@
.project(project)
.forUpdate()
.add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
- .add(allowLabelRemoval(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
.update();
change = gApi.changes().id(r.getChangeId()).get();
@@ -4393,8 +4386,12 @@
ListChangesOption.SKIP_DIFFSTAT);
PushOneCommit.Result change = createChange();
- int number = gApi.changes().id(change.getChangeId()).get()._number;
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
+ int number = gApi.changes().id(change.getChangeId()).get()._number;
try (AutoCloseable ignored = changeIndexOperations.disableReadsAndWrites()) {
assertThat(gApi.changes().id(project.get(), number).get(options).changeId)
.isEqualTo(change.getChangeId());
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index d630296..04bdf15 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -601,6 +601,20 @@
}
@Test
+ public void getGroupFromMetaId() throws Exception {
+ AccountGroup.UUID uuid = groupOperations.newGroup().create();
+ InternalGroup preUpdateState = groupCache.get(uuid).get();
+ gApi.groups().id(uuid.toString()).description("New description");
+
+ InternalGroup postUpdateState = groupCache.get(uuid).get();
+ assertThat(postUpdateState).isNotEqualTo(preUpdateState);
+ assertThat(groupCache.getFromMetaId(uuid, preUpdateState.getRefState()))
+ .isEqualTo(preUpdateState);
+ assertThat(groupCache.getFromMetaId(uuid, postUpdateState.getRefState()))
+ .isEqualTo(postUpdateState);
+ }
+
+ @Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void getSystemGroupByConfiguredName() throws Exception {
GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index a625a70..93f91dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -54,7 +54,7 @@
@Inject private IndexOperations.Project projectIndexOperations;
private static final ImmutableSet<String> FIELDS =
- ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+ ImmutableSet.of(ProjectField.NAME_SPEC.getName(), ProjectField.REF_STATE.getName());
@Test
public void indexProject_indexesRefStateOfProjectAndParents() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index e707949..0291f33 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -22,7 +22,6 @@
import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
@@ -235,15 +234,6 @@
revision(r).review(ReviewInput.recommend());
requestScopeOperations.setApiUser(admin.id());
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
gApi.changes().id(changeId).reviewer(user.username()).deleteVote(LabelId.CODE_REVIEW);
Optional<ApprovalInfo> crUser =
get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream()
@@ -2011,15 +2001,6 @@
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
gApi.changes()
.id(r.getChangeId())
.current()
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index eb827c0..3531d1c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -20,6 +20,8 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.entities.RefNames.patchSetRef;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -28,11 +30,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -43,8 +47,10 @@
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -55,6 +61,7 @@
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -71,8 +78,15 @@
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -105,28 +119,246 @@
}
@Test
+ @UseLocalDisk
public void voteOnBehalfOf() throws Exception {
allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ try (Repository repo = repoManager.openRepository(project)) {
+ String changeMetaRef = changeMetaRef(r.getChange().getId());
+ createRefLogFileIfMissing(repo, changeMetaRef);
+
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // The change meta commit is created by the server and has the impersonated user as the
+ // author.
+ // Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
+ RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
+ assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
+
+ // The ref log for the change meta ref records the impersonated user.
+ ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+ assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithOtherImpersonatedVote_sameValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount realUser2 = admin2;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
- in.onBehalfOf = user.id().toString();
+ in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
- assertThat(psa.accountId()).isEqualTo(user.id());
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
- assertThat(psa.realAccountId()).isEqualTo(admin.id());
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
ChangeData cd = r.getChange();
ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
assertThat(m.getMessage()).endsWith(in.message);
- assertThat(m.getAuthor()).isEqualTo(user.id());
- assertThat(m.getRealAuthor()).isEqualTo(admin.id());
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // realUser2 votes Code-Review+1 on behalf of impersonatedUser, this should override the
+ // impersonated Code-Review+1 of realUser with an impersonated Code-Review+1 of realUser2
+ requestScopeOperations.setApiUser(realUser2.id());
+ in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Another message on behalf of";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser2.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithOtherImpersonatedVote_differentValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount realUser2 = admin2;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // realUser2 votes Code-Review-1 on behalf of impersonatedUser, this should override the
+ // impersonated Code-Review+1 of realUser with an impersonated Code-Review-1 of realUser2
+ requestScopeOperations.setApiUser(realUser2.id());
+ in = ReviewInput.dislike();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Another message on behalf of";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(-1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser2.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithNonImpersonatedVote_sameValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // impersonatedUser votes Code-Review+1 themselves, this should override the impersonated
+ // Code-Review+1 with a non-impersonated Code-Review+1
+ requestScopeOperations.setApiUser(impersonatedUser.id());
+ in = ReviewInput.recommend();
+ in.message = "Message";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(impersonatedUser.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithNonImpersonatedVote_differentValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // impersonatedUser votes Code-Review-1 themselves, this should override the impersonated
+ // Code-Review+1 with a non-impersonated Code-Review-1
+ requestScopeOperations.setApiUser(impersonatedUser.id());
+ in = ReviewInput.dislike();
+ in.message = "Message";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(-1);
+ assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(impersonatedUser.id());
}
@Test
@@ -342,21 +574,120 @@
}
@Test
- public void submitOnBehalfOf() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
+ @UseLocalDisk
+ public void submitOnBehalfOf_mergeAlways() throws Exception {
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = admin2;
+
+ // Create a project with MERGE_ALWAYS submit strategy so that a merge commit is created on
+ // submit and we can verify its committer and author and the ref log for the update of the
+ // target branch.
+ Project.NameKey project =
+ projectOperations.newProject().submitType(SubmitType.MERGE_ALWAYS).create();
+
+ testSubmitOnBehalfOf(project, realUser, impersonatedUser);
+
+ // The merge commit is created by the server and has the impersonated user as the author.
+ RevCommit mergeCommit = projectOperations.project(project).getHead("refs/heads/master");
+ assertThat(mergeCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(mergeCommit.getAuthorIdent().getEmailAddress()).isEqualTo(impersonatedUser.email());
+
+ // The ref log for the target branch records the impersonated user.
+ try (Repository repo = repoManager.openRepository(project)) {
+ ReflogEntry targetBranchRefLogEntry =
+ repo.getReflogReader("refs/heads/master").getLastEntry();
+ assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @Test
+ @UseLocalDisk
+ public void submitOnBehalfOf_rebaseAlways() throws Exception {
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = admin2;
+
+ // Create a project with REBASE_ALWAYS submit strategy so that a new patch set is created on
+ // submit and we can verify its committer and author and the ref log for the update of the
+ // patch set ref and the target branch.
+ Project.NameKey project =
+ projectOperations.newProject().submitType(SubmitType.REBASE_ALWAYS).create();
+
+ ChangeData cd = testSubmitOnBehalfOf(project, realUser, impersonatedUser);
+
+ // Rebase on submit is expected to create a new patch set.
+ assertThat(cd.currentPatchSet().id().get()).isEqualTo(2);
+
+ // The patch set commit is created by the impersonated user and has the real user as the author.
+ // Recording the real user as the author seems to a bug, we would expect the author to be the
+ // impersonated user.
+ RevCommit newPatchSetCommit =
+ projectOperations.project(project).getHead(cd.currentPatchSet().refName());
+ assertThat(newPatchSetCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ assertThat(newPatchSetCommit.getAuthorIdent().getEmailAddress()).isEqualTo(realUser.email());
+
+ try (Repository repo = repoManager.openRepository(project)) {
+ // The ref log for the patch set ref records the impersonated user.
+ ReflogEntry patchSetRefLogEntry =
+ repo.getReflogReader(cd.currentPatchSet().refName()).getLastEntry();
+ assertThat(patchSetRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+
+ // The ref log for the target branch records the impersonated user.
+ ReflogEntry targetBranchRefLogEntry =
+ repo.getReflogReader("refs/heads/master").getLastEntry();
+ assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @CanIgnoreReturnValue
+ private ChangeData testSubmitOnBehalfOf(
+ Project.NameKey project, TestAccount realUser, TestAccount impersonatedUser)
+ throws Exception {
+ allowSubmitOnBehalfOf(project);
+
+ TestRepository<InMemoryRepository> testRepo = cloneProject(project, realUser);
+
+ PushOneCommit.Result r = createChange(testRepo);
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email();
- gApi.changes().id(changeId).current().submit(in);
+ in.onBehalfOf = impersonatedUser.email();
- ChangeData cd = r.getChange();
- assertThat(cd.change().isMerged()).isTrue();
- PatchSetApproval submitter =
- approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
- assertThat(submitter.accountId()).isEqualTo(admin2.id());
- assertThat(submitter.realAccountId()).isEqualTo(admin.id());
+ try (Repository repo = repoManager.openRepository(project)) {
+ String changeMetaRef = changeMetaRef(r.getChange().getId());
+ createRefLogFileIfMissing(repo, changeMetaRef);
+ createRefLogFileIfMissing(repo, "refs/heads/master");
+ createRefLogFileIfMissing(repo, patchSetRef(PatchSet.id(r.getChange().getId(), 2)));
+
+ gApi.changes().id(changeId).current().submit(in);
+
+ ChangeData cd = r.getChange();
+ assertThat(cd.change().isMerged()).isTrue();
+ PatchSetApproval submitter =
+ approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
+ assertThat(submitter.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(submitter.realAccountId()).isEqualTo(realUser.id());
+
+ // The change meta commit is created by the server and has the impersonated user as the
+ // author.
+ // Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
+ RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
+ assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
+
+ // The ref log for the change meta ref records the impersonated user.
+ ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+ assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+
+ return cd;
+ }
}
@Test
@@ -591,6 +922,10 @@
}
private void allowSubmitOnBehalfOf() throws Exception {
+ allowSubmitOnBehalfOf(project);
+ }
+
+ private void allowSubmitOnBehalfOf(Project.NameKey project) throws Exception {
String heads = "refs/heads/*";
projectOperations
.project(project)
@@ -630,4 +965,12 @@
private static Header runAsHeader(Object user) {
return new BasicHeader("X-Gerrit-RunAs", user.toString());
}
+
+ private void createRefLogFileIfMissing(Repository repo, String ref) throws IOException {
+ File log = new File(repo.getDirectory(), "logs/" + ref);
+ if (!log.exists()) {
+ log.getParentFile().mkdirs();
+ assertThat(log.createNewFile()).isTrue();
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 7e8ff62..ea52690 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -16,7 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.extensions.restapi.testing.AttentionSetUpdateSubject.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -1945,16 +1944,6 @@
@Test
public void deleteVotesDoesNotAffectAttentionSetWhenIgnoreAutomaticRulesIsSet() throws Exception {
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
-
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
@@ -1979,16 +1968,6 @@
@Test
public void deleteVotesOfOthersAddThemToAttentionSet() throws Exception {
- projectOperations
- .project(project)
- .forUpdate()
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/heads/*")
- .group(REGISTERED_USERS)
- .range(-2, 2))
- .update();
-
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index c29b265..016b1e6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -15,7 +15,10 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -24,10 +27,12 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -39,15 +44,14 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import org.junit.Before;
import org.junit.Test;
public class DeleteVoteIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
- @Before
- public void allowVoteDeletion() {
+ @Test
+ public void deleteVoteOnChange_withRemoveLabelPermission() throws Exception {
projectOperations
.project(project)
.forUpdate()
@@ -56,57 +60,128 @@
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
.update();
+ verifyDeleteVote(false);
}
@Test
- public void deleteVoteOnChange() throws Exception {
- deleteVote(false);
+ public void deleteVoteOnChange_withRemoveReviewerPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(false);
}
@Test
- public void deleteVoteOnRevision() throws Exception {
- deleteVote(true);
+ public void deleteVoteOnChange_noPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyCannotDeleteVote(false);
}
- private void deleteVote(boolean onRevisionLevel) throws Exception {
+ @Test
+ public void deleteVoteOnRevision_withRemoveLabelPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(true);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_withRemoveReviewerPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(true);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_noPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyCannotDeleteVote(true);
+ }
+
+ private void verifyDeleteVote(boolean onRevisionLevel) throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
PushOneCommit.Result r2 = amendChange(r.getChangeId());
- requestScopeOperations.setApiUser(user.id());
+ requestScopeOperations.setApiUser(admin.id());
+ recommend(r.getChangeId());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
recommend(r.getChangeId());
sender.clear();
- String endPoint =
+ String deleteAdminVoteEndPoint =
"/changes/"
+ r.getChangeId()
+ (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ "/reviewers/"
- + user.id().toString()
+ + admin.id().toString()
+ "/votes/Code-Review";
- RestResponse response = adminRestSession.delete(endPoint);
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
response.assertNoContent();
List<FakeEmailSender.Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
FakeEmailSender.Message msg = messages.get(0);
- assertThat(msg.rcpt()).containsExactly(user.getNameEmail());
- assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change.");
+ assertThat(msg.rcpt()).containsExactly(admin.getNameEmail(), user2.getNameEmail());
+ assertThat(msg.body()).contains(user.fullName() + " has removed a vote from this change.");
assertThat(msg.body())
- .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
+ .contains("Removed Code-Review+1 by " + admin.fullName() + " <" + admin.email() + ">\n");
- endPoint =
+ String viewVotesEndPoint =
"/changes/"
+ r.getChangeId()
+ (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ "/reviewers/"
- + user.id().toString()
+ + admin.id().toString()
+ "/votes";
- response = adminRestSession.get(endPoint);
+ response = userRestSession.get(viewVotesEndPoint);
response.assertOK();
Map<String, Short> m =
@@ -117,14 +192,38 @@
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
ChangeMessageInfo message = Iterables.getLast(c.messages);
- assertThat(message.author._accountId).isEqualTo(admin.id().get());
+ assertThat(message.author._accountId).isEqualTo(user.id().get());
assertThat(message.message)
.isEqualTo(
String.format(
"Removed Code-Review+1 by %s\n",
- AccountTemplateUtil.getAccountTemplate(user.id())));
+ AccountTemplateUtil.getAccountTemplate(admin.id())));
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
- .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
+ .containsExactlyElementsIn(ImmutableSet.of(user2.id(), admin.id()));
+ }
+
+ private void verifyCannotDeleteVote(boolean onRevisionLevel) throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ PushOneCommit.Result r2 = amendChange(r.getChangeId());
+
+ requestScopeOperations.setApiUser(admin.id());
+ recommend(r.getChangeId());
+
+ sender.clear();
+ String deleteAdminVoteEndPoint =
+ "/changes/"
+ + r.getChangeId()
+ + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ + "/reviewers/"
+ + admin.id().toString()
+ + "/votes/Code-Review";
+
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
+ response.assertForbidden();
+
+ assertThat(sender.getMessages()).isEmpty();
}
private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
index 0c221aa..7b42d93 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.rest.project;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.RefNames.REFS_HEADS;
@@ -21,6 +22,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.IdString;
import org.junit.Test;
public class CreateChangeIT extends AbstractDaemonTest {
@@ -43,7 +45,44 @@
ChangeInput input = new ChangeInput();
input.branch = "foo";
input.subject = "subject";
- RestResponse cr = adminRestSession.post("/projects/" + project.get() + "/create.change", input);
- cr.assertCreated();
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertCreated();
+ }
+
+ @Test
+ public void nonMatchingProjectIsRejected() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = "non-matching-project";
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertBadRequest();
+ assertThat(response.getEntityContent()).isEqualTo("project must match URL");
+ }
+
+ @Test
+ public void matchingProjectIsAccepted() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = project.get();
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertCreated();
+ }
+
+ @Test
+ public void matchingProjectWithTrailingSlashIsAccepted() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = project.get() + "/";
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post(
+ "/projects/" + IdString.fromDecoded(project.get() + "/").encoded() + "/create.change",
+ input);
+ response.assertCreated();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index b1277c0..b68afc5 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
import static com.google.gerrit.entities.NotifyConfig.NotifyType.ABANDONED_CHANGES;
import static com.google.gerrit.entities.NotifyConfig.NotifyType.ALL_COMMENTS;
import static com.google.gerrit.entities.NotifyConfig.NotifyType.NEW_CHANGES;
@@ -99,11 +98,6 @@
.add(allow(Permission.SUBMIT_AS).ref("refs/*").group(REGISTERED_USERS))
.add(allow(Permission.ABANDON).ref("refs/*").group(REGISTERED_USERS))
.add(allowLabel(LabelId.CODE_REVIEW).ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
- .add(
- allowLabelRemoval(LabelId.CODE_REVIEW)
- .ref("refs/*")
- .group(REGISTERED_USERS)
- .range(-2, +2))
.update();
}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 5d38c55..fbf9c87 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -4064,7 +4064,7 @@
assertThat(thrown.getCause()).isInstanceOf(QueryParseException.class);
assertThat(thrown)
.hasMessageThat()
- .contains("'is:mergeable' operator is not supported by server");
+ .contains("'is:mergeable' operator is not supported on this gerrit host");
}
protected ChangeInserter newChangeForCommit(TestRepository<Repository> repo, RevCommit commit)
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 540416f..12bafd5 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -401,9 +401,8 @@
String query = "uuid:" + uuid;
assertQuery(query, group);
- for (GroupIndex index : groupIndexes.getWriteIndexes()) {
- index.delete(uuid);
- }
+ deleteGroup(uuid);
+
assertQuery(query);
}
@@ -441,6 +440,10 @@
return createGroupWithDescription(name, null, members);
}
+ protected GroupInfo createGroup(GroupInput in) throws Exception {
+ return gApi.groups().create(in).get();
+ }
+
protected GroupInfo createGroupWithDescription(
String name, String description, AccountInfo... members) throws Exception {
GroupInput in = new GroupInput();
@@ -448,21 +451,27 @@
in.description = description;
in.members =
Arrays.asList(members).stream().map(a -> String.valueOf(a._accountId)).collect(toList());
- return gApi.groups().create(in).get();
+ return createGroup(in);
}
protected GroupInfo createGroupWithOwner(String name, GroupInfo ownerGroup) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
in.ownerId = ownerGroup.id;
- return gApi.groups().create(in).get();
+ return createGroup(in);
}
protected GroupInfo createGroupThatIsVisibleToAll(String name) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
in.visibleToAll = true;
- return gApi.groups().create(in).get();
+ return createGroup(in);
+ }
+
+ protected void deleteGroup(AccountGroup.UUID uuid) throws Exception {
+ for (GroupIndex index : groupIndexes.getWriteIndexes()) {
+ index.delete(uuid);
+ }
}
protected GroupInfo getGroup(AccountGroup.UUID uuid) throws Exception {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
index c3f79c7..0cfbaa4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
@@ -14,7 +14,7 @@
import {LitElement, html, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {resolve} from '../../../models/dependency';
-import {createEditUrl} from '../../../models/views/edit';
+import {createEditUrl} from '../../../models/views/change';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {assertIsDefined} from '../../../utils/common-util';
import {when} from 'lit/directives/when.js';
@@ -162,8 +162,8 @@
const url = createEditUrl({
changeNum: change._number,
repo: change.project,
- path: this.path,
patchNum: 1 as PatchSetNumber,
+ editView: {path: this.path},
});
this.getNavigation().setUrl(url);
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index 115bc43..11cfaab 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -31,7 +31,7 @@
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, query, property, state} from 'lit/decorators.js';
import {assertIsDefined} from '../../../utils/common-util';
-import {createEditUrl} from '../../../models/views/edit';
+import {createEditUrl} from '../../../models/views/change';
import {resolve} from '../../../models/dependency';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {GrCreateFileEditDialog} from '../gr-create-change-dialog/gr-create-file-edit-dialog';
@@ -320,8 +320,8 @@
createEditUrl({
changeNum: change._number,
repo: change.project,
- path: CONFIG_PATH,
patchNum: INITIAL_PATCHSET,
+ editView: {path: CONFIG_PATH},
})
);
})
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
index 632ec4c..7eef7a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
@@ -11,6 +11,7 @@
import '../../shared/gr-list-view/gr-list-view';
import '../gr-create-pointer-dialog/gr-create-pointer-dialog';
import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog';
+import {encodeURL} from '../../../utils/url-util';
import {GrCreatePointerDialog} from '../gr-create-pointer-dialog/gr-create-pointer-dialog';
import {
BranchInfo,
@@ -28,16 +29,12 @@
import {formStyles} from '../../../styles/gr-form-styles';
import {tableStyles} from '../../../styles/gr-table-styles';
import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html, nothing} from 'lit';
+import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, query, property, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {assertIsDefined} from '../../../utils/common-util';
import {ifDefined} from 'lit/directives/if-defined.js';
-import {
- createRepoUrl,
- RepoDetailView,
- RepoViewState,
-} from '../../../models/views/repo';
+import {RepoDetailView, RepoViewState} from '../../../models/views/repo';
import {modalStyles} from '../../../styles/gr-modal-styles';
const PGP_START = '-----BEGIN PGP SIGNATURE-----';
@@ -142,7 +139,6 @@
}
override render() {
- if (!this.repo || !this.detailType) return nothing;
return html`
<gr-list-view
.createNew=${this.loggedIn}
@@ -151,7 +147,7 @@
.items=${this.items}
.loading=${this.loading}
.offset=${this.offset}
- .path=${createRepoUrl({repo: this.repo, detail: this.detailType})}
+ .path=${this.getPath(this.repo, this.detailType)}
@create-clicked=${() => {
this.handleCreateClicked();
}}
@@ -445,6 +441,13 @@
return Promise.reject(new Error('unknown detail type'));
}
+ private getPath(repo?: RepoName, detailType?: RepoDetailView) {
+ // TODO: Replace with `createRepoUrl()`, but be aware that `encodeURL()`
+ // gets `false` as a second parameter here. The router pattern in gr-router
+ // does not handle the filter URLs, if the repo is not encoded!
+ return `/admin/repos/${encodeURL(repo ?? '', false)},${detailType}`;
+ }
+
private computeWeblink(repo: ProjectInfo | BranchInfo | TagInfo) {
if (!repo.web_links) return [];
const webLinks = repo.web_links;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 75854cc..e47b450 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1577,10 +1577,11 @@
base: e.detail.base,
allow_conflicts: e.detail.allowConflicts,
};
+ const rebaseChain = !!e.detail.rebaseChain;
this.fireAction(
- '/rebase',
+ rebaseChain ? '/rebase:chain' : '/rebase',
assertUIActionInfo(this.revisionActions.rebase),
- true,
+ rebaseChain ? false : true,
payload,
{allow_conflicts: payload.allow_conflicts}
);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index c6bfd55..4602eac 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -625,7 +625,9 @@
};
assert.isTrue(fetchChangesStub.called);
element.handleRebaseConfirm(
- new CustomEvent('', {detail: {base: '1234', allowConflicts: false}})
+ new CustomEvent('', {
+ detail: {base: '1234', allowConflicts: false, rebaseChain: false},
+ })
);
assert.deepEqual(fireActionStub.lastCall.args, [
'/rebase',
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index c958c7e..c0ed3b3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -135,10 +135,6 @@
fireTitleChange,
} from '../../../utils/event-util';
import {
- GerritView,
- routerModelToken,
-} from '../../../services/router/router-model';
-import {
debounce,
DelayedTask,
throttleWrap,
@@ -176,12 +172,13 @@
import {getBaseUrl, prependOrigin} from '../../../utils/url-util';
import {CopyLink, GrCopyLinks} from '../gr-copy-links/gr-copy-links';
import {
+ ChangeChildView,
changeViewModelToken,
ChangeViewState,
createChangeUrl,
+ createEditUrl,
} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
-import {createEditUrl} from '../../../models/views/edit';
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -539,8 +536,6 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly getRouterModel = resolve(this, routerModelToken);
-
private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getConfigModel = resolve(this, configModelToken);
@@ -573,7 +568,7 @@
/** Simply reflects the router-model value. */
// visible for testing
- routerPatchNum?: PatchSetNum;
+ viewModelPatchNum?: PatchSetNum;
private readonly shortcutsController = new ShortcutController(this);
@@ -700,16 +695,16 @@
);
subscribe(
this,
- () => this.getRouterModel().routerView$,
- view => {
- this.isViewCurrent = view === GerritView.CHANGE;
+ () => this.getViewModel().childView$,
+ childView => {
+ this.isViewCurrent = childView === ChangeChildView.OVERVIEW;
}
);
subscribe(
this,
- () => this.getRouterModel().routerPatchNum$,
+ () => this.getViewModel().patchNum$,
patchNum => {
- this.routerPatchNum = patchNum;
+ this.viewModelPatchNum = patchNum;
}
);
subscribe(
@@ -1531,7 +1526,6 @@
.allPatchSets=${this.allPatchSets}
.change=${this.change}
.changeNum=${this.changeNum}
- .revisionInfo=${this.getRevisionInfo()}
.commitInfo=${this.commitInfo}
.changeUrl=${this.computeChangeUrl()}
.editMode=${this.getEditMode()}
@@ -2092,13 +2086,6 @@
return;
}
- if (this.viewState.changeNum && this.viewState.repo) {
- this.restApiService.setInProjectLookup(
- this.viewState.changeNum,
- this.viewState.repo
- );
- }
-
if (this.viewState.basePatchNum === undefined)
this.viewState.basePatchNum = PARENT;
@@ -2284,7 +2271,7 @@
private updateTitle(change?: ChangeInfo | ParsedChangeInfo) {
if (!change) return;
- const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
+ const title = `${change.subject} (${change._number})`;
fireTitleChange(this, title);
}
@@ -2448,6 +2435,7 @@
// Private but used in tests.
handleDiffBaseAgainstLeft() {
+ if (this.viewState?.childView !== ChangeChildView.OVERVIEW) return;
assertIsDefined(this.change, 'change');
assertIsDefined(this.patchRange, 'patchRange');
@@ -2650,7 +2638,7 @@
// is under change-model control. `patchRange.patchNum` should eventually
// also be model managed, so we can reconcile these two code snippets into
// one location.
- if (!this.routerPatchNum && latestPsNum === editParentRev._number) {
+ if (!this.viewModelPatchNum && latestPsNum === editParentRev._number) {
this.patchRange = {...this.patchRange, patchNum: EDIT};
// The file list is not reactive (yet) with regards to patch range
// changes, so we have to actively trigger it.
@@ -3143,8 +3131,8 @@
createEditUrl({
changeNum: this.change._number,
repo: this.change.project,
- path,
patchNum: this.patchRange.patchNum,
+ editView: {path},
})
);
break;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 9d89192..9b78e64 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -99,7 +99,7 @@
import {Modifier} from '../../../utils/dom-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrCopyLinks} from '../gr-copy-links/gr-copy-links';
-import {ChangeViewState} from '../../../models/views/change';
+import {ChangeChildView, ChangeViewState} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
import {UserModel, userModelToken} from '../../../models/user/user-model';
@@ -369,6 +369,7 @@
);
element.viewState = {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
changeNum: TEST_NUMERIC_CHANGE_ID,
repo: 'gerrit' as RepoName,
};
@@ -1986,7 +1987,7 @@
// When edit is set, but patchNum as well, then keep patchNum.
element.patchRange.patchNum = 5 as RevisionPatchSetNum;
- element.routerPatchNum = 5 as RevisionPatchSetNum;
+ element.viewModelPatchNum = 5 as RevisionPatchSetNum;
element.processEdit(change);
assert.equal(element.patchRange.patchNum, 5 as RevisionPatchSetNum);
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index da61b60..b0dbda5 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -5,6 +5,7 @@
*/
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
+import {when} from 'lit/directives/when.js';
import {
NumericChangeId,
BranchName,
@@ -21,6 +22,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {ValueChangedEvent} from '../../../types/events';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {KnownExperimentId} from '../../../services/flags/flags';
export interface RebaseChange {
name: string;
@@ -30,6 +32,7 @@
export interface ConfirmRebaseEventDetail {
base: string | null;
allowConflicts: boolean;
+ rebaseChain: boolean;
}
@customElement('gr-confirm-rebase-dialog')
@@ -85,11 +88,16 @@
@query('#rebaseAllowConflicts')
private rebaseAllowConflicts!: HTMLInputElement;
+ @query('#rebaseChain')
+ private rebaseChain?: HTMLInputElement;
+
@query('#parentInput')
parentInput!: GrAutocomplete;
private readonly restApiService = getAppContext().restApiService;
+ private readonly flagsService = getAppContext().flagsService;
+
constructor() {
super();
this.query = input => this.getChangeSuggestions(input);
@@ -221,6 +229,14 @@
>Allow rebase with conflicts</label
>
</div>
+ ${when(
+ this.flagsService.isEnabled(KnownExperimentId.REBASE_CHAIN),
+ () =>
+ html`<div>
+ <input id="rebaseChain" type="checkbox" />
+ <label for="rebaseChain">Rebase all ancestors</label>
+ </div>`
+ )}
</div>
</gr-dialog>
`;
@@ -326,6 +342,7 @@
const detail: ConfirmRebaseEventDetail = {
base: this.getSelectedBase(),
allowConflicts: this.rebaseAllowConflicts.checked,
+ rebaseChain: !!this.rebaseChain?.checked,
};
this.dispatchEvent(new CustomEvent('confirm', {detail}));
this.text = '';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index 89a1ff5..c1e866c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -20,8 +20,6 @@
PatchSetNum,
CommitInfo,
ServerInfo,
- RevisionInfo,
- NumericChangeId,
BasePatchSetNum,
} from '../../../types/common';
import {DiffPreferencesInfo} from '../../../types/diff';
@@ -71,9 +69,6 @@
change: ChangeInfo | undefined;
@property({type: String})
- changeNum?: NumericChangeId;
-
- @property({type: String})
changeUrl?: string;
@property({type: Object})
@@ -97,9 +92,6 @@
@property({type: String})
filesExpanded?: FilesExpandedState;
- @property({type: Object})
- revisionInfo?: RevisionInfo;
-
@state()
diffPrefs?: DiffPreferencesInfo;
@@ -274,12 +266,6 @@
<div class="patchInfoContent">
<gr-patch-range-select
id="rangeSelect"
- .changeNum=${this.changeNum}
- .patchNum=${this.patchNum}
- .basePatchNum=${this.basePatchNum}
- .availablePatches=${this.allPatchSets}
- .revisions=${this.change.revisions}
- .revisionInfo=${this.revisionInfo}
@patch-range-change=${this.handlePatchChange}
>
</gr-patch-range-select>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
index 23534a0..6c2282b 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
@@ -18,7 +18,6 @@
import {
BasePatchSetNum,
ChangeId,
- NumericChangeId,
PARENT,
PatchSetNum,
PatchSetNumber,
@@ -174,7 +173,6 @@
});
test('show/hide diffs disabled for large amounts of files', async () => {
- element.changeNum = 42 as NumericChangeId;
element.basePatchNum = PARENT;
element.patchNum = '2' as PatchSetNum;
element.shownFileCount = 1;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 071c489..d4defcb 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -78,9 +78,11 @@
import {incrementalRepeat} from '../../lit/incremental-repeat';
import {ifDefined} from 'lit/directives/if-defined.js';
import {HtmlPatched} from '../../../utils/lit-util';
-import {createDiffUrl} from '../../../models/views/diff';
-import {createEditUrl} from '../../../models/views/edit';
-import {createChangeUrl} from '../../../models/views/change';
+import {
+ createDiffUrl,
+ createEditUrl,
+ createChangeUrl,
+} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {FileMode, fileModeToString} from '../../../utils/file-util';
@@ -758,7 +760,7 @@
);
subscribe(
this,
- () => this.getFilesModel().filesWithUnmodified$,
+ () => this.getFilesModel().filesIncludingUnmodified$,
files => {
this.files = [...files];
}
@@ -2121,9 +2123,9 @@
this.getNavigation().setUrl(
createDiffUrl({
change: this.change,
- path: diff.path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
+ diffView: {path: diff.path},
})
);
}
@@ -2142,9 +2144,9 @@
this.getNavigation().setUrl(
createDiffUrl({
change: this.change,
- path: this.files[this.fileCursor.index].__path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
+ diffView: {path: this.files[this.fileCursor.index].__path},
})
);
}
@@ -2176,16 +2178,16 @@
return createEditUrl({
changeNum: this.change._number,
repo: this.change.project,
- path,
patchNum: this.patchRange.patchNum,
+ editView: {path},
});
}
return createDiffUrl({
changeNum: this.change._number,
repo: this.change.project,
- path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
+ diffView: {path},
});
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 3fdb1c0..5792230 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -75,8 +75,7 @@
import {HtmlPatched} from '../../utils/lit-util';
import {DropdownItem} from '../shared/gr-dropdown-list/gr-dropdown-list';
import './gr-checks-attempt';
-import {createDiffUrl} from '../../models/views/diff';
-import {changeViewModelToken} from '../../models/views/change';
+import {createDiffUrl, changeViewModelToken} from '../../models/views/change';
/**
* Firing this event sets the regular expression of the results filter.
@@ -715,9 +714,8 @@
url: createDiffUrl({
changeNum: change._number,
repo: change.project,
- path,
patchNum: patchset,
- lineNum: line,
+ diffView: {path, lineNum: line},
}),
primary: true,
};
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index b1ff749..833a91a 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -219,6 +219,7 @@
}
.titleText::after {
content: var(--header-title-content);
+ white-space: nowrap;
}
ul {
list-style: none;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 1b914bf..bcf6937 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -10,7 +10,11 @@
} from '../../../utils/page-wrapper-utils';
import {NavigationService} from '../gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
-import {convertToPatchSetNum} from '../../../utils/patch-set-util';
+import {
+ computeAllPatchSets,
+ computeLatestPatchNum,
+ convertToPatchSetNum,
+} from '../../../utils/patch-set-util';
import {assertIsDefined} from '../../../utils/common-util';
import {
BasePatchSetNum,
@@ -27,7 +31,7 @@
import {AppElement, AppElementParams} from '../../gr-app-types';
import {LocationChangeEventDetail} from '../../../types/events';
import {GerritView, RouterModel} from '../../../services/router/router-model';
-import {firePageError} from '../../../utils/event-util';
+import {fireAlert, firePageError} from '../../../utils/event-util';
import {windowLocationReload} from '../../../utils/dom-util';
import {
getBaseUrl,
@@ -61,13 +65,13 @@
GroupViewModel,
GroupViewState,
} from '../../../models/views/group';
-import {DiffViewModel, DiffViewState} from '../../../models/views/diff';
import {
+ ChangeChildView,
ChangeViewModel,
ChangeViewState,
- createChangeUrl,
+ createChangeViewUrl,
+ createDiffUrl,
} from '../../../models/views/change';
-import {EditViewModel, EditViewState} from '../../../models/views/edit';
import {
DashboardViewModel,
DashboardViewState,
@@ -88,6 +92,13 @@
import {SearchViewModel, SearchViewState} from '../../../models/views/search';
import {DashboardSection} from '../../../utils/dashboard-util';
import {Subscription} from 'rxjs';
+import {
+ addPath,
+ findComment,
+ getPatchRangeForCommentUrl,
+ isInBaseOfPatchRange,
+} from '../../../utils/comment-util';
+import {isFileUnchanged} from '../../../embed/diff/gr-diff/gr-diff-utils';
const RoutePattern = {
ROOT: '/',
@@ -303,9 +314,7 @@
private readonly agreementViewModel: AgreementViewModel,
private readonly changeViewModel: ChangeViewModel,
private readonly dashboardViewModel: DashboardViewModel,
- private readonly diffViewModel: DiffViewModel,
private readonly documentationViewModel: DocumentationViewModel,
- private readonly editViewModel: EditViewModel,
private readonly groupViewModel: GroupViewModel,
private readonly pluginViewModel: PluginViewModel,
private readonly repoViewModel: RepoViewModel,
@@ -322,7 +331,7 @@
// So this check is slightly fragile, but should work.
if (this.view !== GerritView.CHANGE) return;
const browserUrl = new URL(window.location.toString());
- const stateUrl = new URL(createChangeUrl(state), browserUrl);
+ const stateUrl = new URL(createChangeViewUrl(state), browserUrl);
// Keeping the hash and certain parameters are stop-gap solution. We
// should find better ways of maintaining an overall consistent URL
@@ -363,13 +372,14 @@
if ('repo' in state && state.repo !== undefined && 'changeNum' in state)
this.restApiService.setInProjectLookup(state.changeNum, state.repo);
- this.routerModel.setState({
- view: state.view,
- changeNum: 'changeNum' in state ? state.changeNum : undefined,
- patchNum: 'patchNum' in state ? state.patchNum ?? undefined : undefined,
- basePatchNum:
- 'basePatchNum' in state ? state.basePatchNum ?? undefined : undefined,
- });
+ this.routerModel.setState({view: state.view});
+ // We are trying to reset the change (view) model when navigating to other
+ // views, because we don't trust our reset logic at the moment. The models
+ // singletons and might unintentionally keep state from one change to
+ // another. TODO: Let's find some way to avoid that.
+ if (state.view !== GerritView.CHANGE) {
+ this.changeViewModel.setState(undefined);
+ }
this.appElement().params = state;
}
@@ -1441,6 +1451,7 @@
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]) as RevisionPatchSetNum,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
};
const queryMap = new URLSearchParams(ctx.querystring);
@@ -1471,21 +1482,57 @@
this.changeViewModel.setState(state);
}
- handleCommentRoute(ctx: PageContext) {
+ async handleCommentRoute(ctx: PageContext) {
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- const state: DiffViewState = {
- repo: ctx.params[0] as RepoName,
+ const repo = ctx.params[0] as RepoName;
+ const commentId = ctx.params[2] as UrlEncodedCommentId;
+
+ const comments = await this.restApiService.getDiffComments(changeNum);
+ const change = await this.restApiService.getChangeDetail(changeNum);
+
+ const comment = findComment(addPath(comments), commentId);
+ const path = comment?.path;
+ const patchsets = computeAllPatchSets(change);
+ const latestPatchNum = computeLatestPatchNum(patchsets);
+ if (!comment || !path || !latestPatchNum) {
+ this.show404();
+ return;
+ }
+ let {basePatchNum, patchNum} = getPatchRangeForCommentUrl(
+ comment,
+ latestPatchNum
+ );
+
+ if (basePatchNum !== PARENT) {
+ const diff = await this.restApiService.getDiff(
+ changeNum,
+ basePatchNum,
+ patchNum,
+ path
+ );
+ if (diff && isFileUnchanged(diff)) {
+ fireAlert(
+ document,
+ `File is unchanged between Patchset ${basePatchNum} and ${patchNum}.
+ Showing diff of Base vs ${basePatchNum}.`
+ );
+ patchNum = basePatchNum as RevisionPatchSetNum;
+ basePatchNum = PARENT;
+ }
+ }
+
+ const diffUrl = createDiffUrl({
changeNum,
- commentId: ctx.params[2] as UrlEncodedCommentId,
- view: GerritView.DIFF,
- commentLink: true,
- };
- this.reporting.setRepoName(state.repo ?? '');
- this.reporting.setChangeId(changeNum);
- this.normalizePatchRangeParams(state);
- // Note that router model view must be updated before view models.
- this.setState(state);
- this.diffViewModel.setState(state);
+ repo,
+ patchNum,
+ basePatchNum,
+ diffView: {
+ path,
+ lineNum: comment.line,
+ leftSide: isInBaseOfPatchRange(comment, {basePatchNum, patchNum}),
+ },
+ });
+ this.redirect(diffUrl);
}
handleCommentsRoute(ctx: PageContext) {
@@ -1495,6 +1542,7 @@
changeNum,
commentId: ctx.params[2] as UrlEncodedCommentId,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
};
assertIsDefined(state.repo);
this.reporting.setRepoName(state.repo);
@@ -1508,25 +1556,26 @@
handleDiffRoute(ctx: PageContext) {
const changeNum = Number(ctx.params[1]) as NumericChangeId;
// Parameter order is based on the regex group number matched.
- const state: DiffViewState = {
+ const state: ChangeViewState = {
repo: ctx.params[0] as RepoName,
changeNum,
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]) as RevisionPatchSetNum,
- path: ctx.params[8],
- view: GerritView.DIFF,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.DIFF,
+ diffView: {path: ctx.params[8]},
};
const address = this.parseLineAddress(ctx.hash);
if (address) {
- state.leftSide = address.leftSide;
- state.lineNum = address.lineNum;
+ state.diffView!.leftSide = address.leftSide;
+ state.diffView!.lineNum = address.lineNum;
}
this.reporting.setRepoName(state.repo ?? '');
this.reporting.setChangeId(changeNum);
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
this.setState(state);
- this.diffViewModel.setState(state);
+ this.changeViewModel.setState(state);
}
handleChangeLegacyRoute(ctx: PageContext) {
@@ -1554,19 +1603,19 @@
// Parameter order is based on the regex group number matched.
const project = ctx.params[0] as RepoName;
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- const state: EditViewState = {
+ const state: ChangeViewState = {
repo: project,
changeNum,
// for edit view params, patchNum cannot be undefined
patchNum: convertToPatchSetNum(ctx.params[2]) as RevisionPatchSetNum,
- path: ctx.params[3],
- lineNum: Number(ctx.hash),
- view: GerritView.EDIT,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
+ editView: {path: ctx.params[3], lineNum: Number(ctx.hash)},
};
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
this.setState(state);
- this.editViewModel.setState(state);
+ this.changeViewModel.setState(state);
this.reporting.setRepoName(project);
this.reporting.setChangeId(changeNum);
}
@@ -1581,6 +1630,7 @@
changeNum,
patchNum: convertToPatchSetNum(ctx.params[3]) as RevisionPatchSetNum,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
edit: true,
};
const tab = queryMap.get('tab');
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index b8f68e6..d8761bf 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -28,10 +28,16 @@
import {AdminChildView} from '../../../models/views/admin';
import {RepoDetailView} from '../../../models/views/repo';
import {GroupDetailView} from '../../../models/views/group';
-import {EditViewState} from '../../../models/views/edit';
-import {ChangeViewState} from '../../../models/views/change';
+import {ChangeChildView, ChangeViewState} from '../../../models/views/change';
import {PatchRangeParams} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
+import {
+ createComment,
+ createDiff,
+ createParsedChange,
+ createRevision,
+} from '../../../test/test-data-generators';
+import {ParsedChangeInfo} from '../../../types/types';
suite('gr-router tests', () => {
let router: GrRouter;
@@ -1134,6 +1140,7 @@
const ctx = makeParams('', '');
assertctxToParams(ctx, 'handleChangeRoute', {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
@@ -1154,6 +1161,7 @@
ctx.querystring = queryMap.toString();
assertctxToParams(ctx, 'handleChangeRoute', {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
@@ -1193,36 +1201,89 @@
test('diff view', () => {
const ctx = makeParams('foo/bar/baz', 'b44');
assertctxToParams(ctx, 'handleDiffRoute', {
- view: GerritView.DIFF,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.DIFF,
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
patchNum: 7 as RevisionPatchSetNum,
- path: 'foo/bar/baz',
- leftSide: true,
- lineNum: 44,
+ diffView: {
+ path: 'foo/bar/baz',
+ lineNum: 44,
+ leftSide: true,
+ },
});
assert.isFalse(redirectStub.called);
});
- test('comment route', () => {
- const url = '/c/gerrit/+/264833/comment/00049681_f34fd6a9/';
+ test('comment route base..1', async () => {
+ const change: ParsedChangeInfo = createParsedChange();
+ const repo = change.project;
+ const changeNum = change._number;
+ const ps = 1 as RevisionPatchSetNum;
+ const line = 23;
+ const id = '00049681_f34fd6a9' as UrlEncodedCommentId;
+ stubRestApi('getChangeDetail').resolves(change);
+ stubRestApi('getDiffComments').resolves({
+ filepath: [{...createComment(), id, patch_set: ps, line}],
+ });
+
+ const url = `/c/${repo}/+/${changeNum}/comment/${id}/`;
const groups = url.match(_testOnly_RoutePattern.COMMENT);
- assert.deepEqual(groups!.slice(1), [
- 'gerrit', // project
- '264833', // changeNum
- '00049681_f34fd6a9', // commentId
- ]);
- assertctxToParams(
- {params: groups!.slice(1)} as any,
- 'handleCommentRoute',
- {
- repo: 'gerrit' as RepoName,
- changeNum: 264833 as NumericChangeId,
- commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
- commentLink: true,
- view: GerritView.DIFF,
- }
+ assert.deepEqual(groups!.slice(1), [repo, `${changeNum}`, id]);
+
+ await router.handleCommentRoute({params: groups!.slice(1)} as any);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ `/c/${repo}/+/${changeNum}/${ps}/filepath#${line}`
+ );
+ });
+
+ test('comment route 1..2', async () => {
+ const change: ParsedChangeInfo = {
+ ...createParsedChange(),
+ revisions: {
+ abc: createRevision(1),
+ def: createRevision(2),
+ },
+ };
+ const repo = change.project;
+ const changeNum = change._number;
+ const ps = 1 as RevisionPatchSetNum;
+ const line = 23;
+ const id = '00049681_f34fd6a9' as UrlEncodedCommentId;
+
+ stubRestApi('getChangeDetail').resolves(change);
+ stubRestApi('getDiffComments').resolves({
+ filepath: [{...createComment(), id, patch_set: ps, line}],
+ });
+ const diffStub = stubRestApi('getDiff');
+
+ const url = `/c/${repo}/+/${changeNum}/comment/${id}/`;
+ const groups = url.match(_testOnly_RoutePattern.COMMENT);
+
+ // If getDiff() returns a diff with changes, then we will compare
+ // the patchset of the comment (1) against latest (2).
+ diffStub.onFirstCall().resolves(createDiff());
+ await router.handleCommentRoute({params: groups!.slice(1)} as any);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ `/c/${repo}/+/${changeNum}/${ps}..2/filepath#b${line}`
+ );
+
+ // If getDiff() returns an unchanged diff, then we will compare
+ // the patchset of the comment (1) against base.
+ diffStub.onSecondCall().resolves({
+ ...createDiff(),
+ content: [],
+ });
+ await router.handleCommentRoute({params: groups!.slice(1)} as any);
+ assert.isTrue(redirectStub.calledTwice);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ `/c/${repo}/+/${changeNum}/${ps}/filepath#${line}`
);
});
@@ -1242,6 +1303,7 @@
changeNum: 264833 as NumericChangeId,
commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
}
);
});
@@ -1259,13 +1321,13 @@
3: 'foo/bar/baz', // 3 File path
},
};
- const appParams: EditViewState = {
+ const appParams: ChangeViewState = {
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
- view: GerritView.EDIT,
- path: 'foo/bar/baz',
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
patchNum: 3 as RevisionPatchSetNum,
- lineNum: 0,
+ editView: {path: 'foo/bar/baz', lineNum: 0},
};
router.handleDiffEditRoute(ctx);
@@ -1285,13 +1347,13 @@
3: 'foo/bar/baz', // 3 File path
},
};
- const appParams: EditViewState = {
+ const appParams: ChangeViewState = {
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
- view: GerritView.EDIT,
- path: 'foo/bar/baz',
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
patchNum: 3 as RevisionPatchSetNum,
- lineNum: 4,
+ editView: {path: 'foo/bar/baz', lineNum: 4},
};
router.handleDiffEditRoute(ctx);
@@ -1314,6 +1376,7 @@
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
patchNum: 3 as RevisionPatchSetNum,
edit: true,
};
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index 26043b32..6eb5243 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -7,7 +7,6 @@
PatchRange,
PatchSetNum,
RobotCommentInfo,
- UrlEncodedCommentId,
PathToCommentsInfoMap,
FileInfo,
PARENT,
@@ -64,26 +63,6 @@
return this._drafts;
}
- findCommentById(
- commentId?: UrlEncodedCommentId
- ): CommentInfo | DraftInfo | undefined {
- if (!commentId) return undefined;
- const findComment = (comments: {
- [path: string]: (CommentInfo | DraftInfo)[];
- }) => {
- let comment;
- for (const path of Object.keys(comments)) {
- comment = comment || comments[path].find(c => c.id === commentId);
- }
- return comment;
- };
- return (
- findComment(this._comments) ||
- findComment(this._robotComments) ||
- findComment(this._drafts)
- );
- }
-
/**
* Get an object mapping file paths to a boolean representing whether that
* path contains diff comments in the given patch set (including drafts and
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index eb2b494..de92b16 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -26,12 +26,7 @@
isInBaseOfPatchRange,
isInRevisionOfPatchRange,
} from '../../../utils/comment-util';
-import {
- CommitRange,
- CoverageRange,
- DiffLayer,
- PatchSetFile,
-} from '../../../types/types';
+import {CoverageRange, DiffLayer, PatchSetFile} from '../../../types/types';
import {
Base64ImageFile,
BlameInfo,
@@ -192,9 +187,6 @@
fire(this, 'is-image-diff-changed', {value: isImageDiff});
}
- @property({type: Object})
- commitRange?: CommitRange;
-
@state()
private _editWeblinks?: GeneratedWebLink[];
@@ -607,7 +599,11 @@
this.hasReloadBeenCalledOnce = true;
this.reporting.time(Timing.DIFF_TOTAL);
this.reporting.time(Timing.DIFF_LOAD);
+ // TODO: Find better names for these 3 clear/cancel methods. Ideally the
+ // <gr-diff-host> should not re-used at all for another diff rendering pass.
this.clear();
+ this.cancel();
+ this.clearDiffContent();
assertIsDefined(this.path, 'path');
assertIsDefined(this.changeNum, 'changeNum');
this.diff = undefined;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index d17c858..e9da5b6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -20,21 +20,12 @@
import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
import '../gr-patch-range-select/gr-patch-range-select';
import '../../change/gr-download-dialog/gr-download-dialog';
-import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
+import {isMergeParent, getParentIndex} from '../../../utils/patch-set-util';
import {
- computeAllPatchSets,
- computeLatestPatchNum,
- PatchSet,
- isMergeParent,
- getParentIndex,
-} from '../../../utils/patch-set-util';
-import {
- addUnmodifiedFiles,
computeDisplayPath,
computeTruncatedPath,
isMagicPath,
- specialFilePathCompare,
} from '../../../utils/path-list-util';
import {changeBaseURL, changeIsOpen} from '../../../utils/change-util';
import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
@@ -45,52 +36,33 @@
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {
BasePatchSetNum,
- ChangeInfo,
- CommitId,
EDIT,
- FileInfo,
NumericChangeId,
PARENT,
PatchRange,
- PatchSetNum,
PatchSetNumber,
PreferencesInfo,
RepoName,
- RevisionInfo,
RevisionPatchSetNum,
ServerInfo,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {
- CommitRange,
- EditRevisionInfo,
- FileRange,
- ParsedChangeInfo,
-} from '../../../types/types';
+import {FileRange, ParsedChangeInfo} from '../../../types/types';
import {FilesWebLinks} from '../gr-patch-range-select/gr-patch-range-select';
import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
import {GrApplyFixDialog} from '../gr-apply-fix-dialog/gr-apply-fix-dialog';
-import {RevisionInfo as RevisionInfoObj} from '../../shared/revision-info/revision-info';
-import {
- CommentMap,
- getPatchRangeForCommentUrl,
- isInBaseOfPatchRange,
-} from '../../../utils/comment-util';
+import {CommentMap} from '../../../utils/comment-util';
import {
EventType,
OpenFixPreviewEvent,
ValueChangedEvent,
} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
-import {
- GerritView,
- routerModelToken,
-} from '../../../services/router/router-model';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
import {Key, toggleClass, whenVisible} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
-import {isFalse, throttleWrap, until} from '../../../utils/async-util';
+import {throttleWrap} from '../../../utils/async-util';
import {filter, take, switchMap} from 'rxjs/operators';
import {combineLatest} from 'rxjs';
import {
@@ -98,14 +70,12 @@
ShortcutSection,
shortcutsServiceToken,
} from '../../../services/shortcuts/shortcuts-service';
-import {LoadingStatus} from '../../../models/change/change-model';
import {DisplayLine} from '../../../api/diff';
import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
import {resolve} from '../../../models/dependency';
-import {BehaviorSubject} from 'rxjs';
-import {css, html, LitElement, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {ShortcutController} from '../../lit/shortcut-controller';
import {subscribe} from '../../lit/subscription-controller';
import {customElement, property, query, state} from 'lit/decorators.js';
@@ -116,16 +86,18 @@
import {when} from 'lit/directives/when.js';
import {
createDiffUrl,
- diffViewModelToken,
- DiffViewState,
-} from '../../../models/views/diff';
-import {createChangeUrl} from '../../../models/views/change';
-import {createEditUrl} from '../../../models/views/edit';
+ ChangeChildView,
+ changeViewModelToken,
+} from '../../../models/views/change';
import {GeneratedWebLink} from '../../../utils/weblink-util';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
import {GrDiffPreferencesDialog} from '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
+import {
+ FileNameToNormalizedFileInfoMap,
+ filesModelToken,
+} from '../../../models/change/files-model';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -135,14 +107,11 @@
// visible for testing
export interface Files {
- sortedFileList: string[];
- changeFilesByPath: {[path: string]: FileInfo};
+ /** All file paths sorted by `specialFilePathCompare`. */
+ sortedPaths: string[];
+ changeFilesByPath: FileNameToNormalizedFileInfoMap;
}
-interface CommentSkips {
- previous: string | null;
- next: string | null;
-}
@customElement('gr-diff-view')
export class GrDiffView extends LitElement {
/**
@@ -159,8 +128,8 @@
@query('#diffHost')
diffHost?: GrDiffHost;
- @query('#reviewed')
- reviewed?: HTMLInputElement;
+ @state()
+ reviewed = false;
@query('#downloadModal')
downloadModal?: HTMLDialogElement;
@@ -177,33 +146,31 @@
@query('#diffPreferencesDialog')
diffPreferencesDialog?: GrDiffPreferencesDialog;
- private _viewState: DiffViewState | undefined;
-
+ // Private but used in tests.
@state()
- get viewState(): DiffViewState | undefined {
- return this._viewState;
- }
-
- set viewState(viewState: DiffViewState | undefined) {
- if (this._viewState === viewState) return;
- const oldViewState = this._viewState;
- this._viewState = viewState;
- this.viewStateChanged();
- this.requestUpdate('viewState', oldViewState);
+ get patchRange(): PatchRange | undefined {
+ if (!this.patchNum) return undefined;
+ return {
+ patchNum: this.patchNum,
+ basePatchNum: this.basePatchNum,
+ };
}
// Private but used in tests.
@state()
- patchRange?: PatchRange;
+ patchNum?: RevisionPatchSetNum;
// Private but used in tests.
@state()
- commitRange?: CommitRange;
+ basePatchNum: BasePatchSetNum = PARENT;
// Private but used in tests.
@state()
change?: ParsedChangeInfo;
+ @state()
+ latestPatchNum?: PatchSetNumber;
+
// Private but used in tests.
@state()
changeComments?: ChangeComments;
@@ -216,10 +183,9 @@
@state()
diff?: DiffInfo;
- // TODO: Move to using files-model.
// Private but used in tests.
@state()
- files: Files = {sortedFileList: [], changeFilesByPath: {}};
+ files: Files = {sortedPaths: [], changeFilesByPath: {}};
// Private but used in tests
// Use path getter/setter.
@@ -237,13 +203,13 @@
this.requestUpdate('path', oldPath);
}
+ /** Allows us to react when the user switches to the DIFF view. */
// Private but used in tests.
- @state()
- loggedIn = false;
+ @state() isActiveChildView = false;
// Private but used in tests.
@state()
- loading = true;
+ loggedIn = false;
@property({type: Object})
prefs?: DiffPreferencesInfo;
@@ -266,65 +232,61 @@
// Private but used in tests.
@state()
- commentMap?: CommentMap;
-
- @state()
- private commentSkips?: CommentSkips;
-
- // Private but used in tests.
- @state()
isBlameLoaded?: boolean;
@state()
private isBlameLoading = false;
- @state()
- private allPatchSets?: PatchSet[] = [];
-
+ /** Directly reflects the view model property `diffView.lineNum`. */
// Private but used in tests.
@state()
focusLineNum?: number;
+ /** Directly reflects the view model property `diffView.leftSide`. */
+ @state()
+ leftSide = false;
+
// visible for testing
reviewedFiles = new Set<string>();
private readonly reporting = getAppContext().reportingService;
- private readonly restApiService = getAppContext().restApiService;
-
- private readonly getRouterModel = resolve(this, routerModelToken);
-
private readonly getUserModel = resolve(this, userModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
private readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getFilesModel = resolve(this, filesModelToken);
+
private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
private readonly getConfigModel = resolve(this, configModelToken);
- private readonly getViewModel = resolve(this, diffViewModelToken);
+ private readonly getViewModel = resolve(this, changeViewModelToken);
private throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
@state()
cursor?: GrDiffCursor;
- private connected$ = new BehaviorSubject(false);
-
private readonly shortcutsController = new ShortcutController(this);
- private readonly getNavigation = resolve(this, navigationToken);
-
constructor() {
super();
this.setupKeyboardShortcuts();
this.setupSubscriptions();
subscribe(
this,
- () => this.getViewModel().state$,
- x => (this.viewState = x)
+ () => this.getFilesModel().filesIncludingUnmodified$,
+ files => {
+ const filesByPath: FileNameToNormalizedFileInfoMap = {};
+ for (const f of files) filesByPath[f.__path] = f;
+ this.files = {
+ sortedPaths: files.map(f => f.__path),
+ changeFilesByPath: filesByPath,
+ };
+ }
);
}
@@ -338,10 +300,10 @@
listen(Shortcut.PREV_LINE, _ => this.handlePrevLine());
listen(Shortcut.VISIBLE_LINE, _ => this.cursor?.moveToVisibleArea());
listen(Shortcut.NEXT_FILE_WITH_COMMENTS, _ =>
- this.moveToNextFileWithComment()
+ this.moveToFileWithComment(1)
);
listen(Shortcut.PREV_FILE_WITH_COMMENTS, _ =>
- this.moveToPreviousFileWithComment()
+ this.moveToFileWithComment(-1)
);
listen(Shortcut.NEW_COMMENT, _ => this.handleNewComment());
listen(Shortcut.SAVE_COMMENT, _ => {});
@@ -354,7 +316,9 @@
listen(Shortcut.OPEN_REPLY_DIALOG, _ => this.handleOpenReplyDialog());
listen(Shortcut.TOGGLE_LEFT_PANE, _ => this.handleToggleLeftPane());
listen(Shortcut.OPEN_DOWNLOAD_DIALOG, _ => this.handleOpenDownloadDialog());
- listen(Shortcut.UP_TO_CHANGE, _ => this.handleUpToChange());
+ listen(Shortcut.UP_TO_CHANGE, _ =>
+ this.getChangeModel().navigateToChange()
+ );
listen(Shortcut.OPEN_DIFF_PREFS, _ => this.handleCommaKey());
listen(Shortcut.TOGGLE_DIFF_MODE, _ => this.handleToggleDiffMode());
listen(Shortcut.TOGGLE_FILE_REVIEWED, e => {
@@ -437,6 +401,11 @@
);
subscribe(
this,
+ () => this.getChangeModel().latestPatchNum$,
+ latestPatchNum => (this.latestPatchNum = latestPatchNum)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().reviewedFiles$,
reviewedFiles => {
this.reviewedFiles = new Set(reviewedFiles) ?? new Set();
@@ -444,45 +413,82 @@
);
subscribe(
this,
- () => this.getChangeModel().diffPath$,
+ () => this.getViewModel().changeNum$,
+ changeNum => {
+ if (!changeNum || this.changeNum === changeNum) return;
+
+ // We are only setting the changeNum of the diff view once!
+ // Everything in the diff view is tied to the change. It seems better to
+ // force the re-creation of the diff view when the change number changes.
+ if (!this.changeNum) {
+ this.changeNum = changeNum;
+ } else {
+ fireEvent(this, EventType.RECREATE_DIFF_VIEW);
+ }
+ }
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().childView$,
+ childView => (this.isActiveChildView = childView === ChangeChildView.DIFF)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().diffPath$,
path => (this.path = path)
);
-
+ subscribe(
+ this,
+ () => this.getViewModel().diffLine$,
+ line => (this.focusLineNum = line)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().diffLeftSide$,
+ leftSide => (this.leftSide = leftSide)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().patchNum$,
+ patchNum => (this.patchNum = patchNum)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().basePatchNum$,
+ basePatchNum => (this.basePatchNum = basePatchNum ?? PARENT)
+ );
subscribe(
this,
() =>
combineLatest([
- this.getChangeModel().diffPath$,
+ this.getViewModel().diffPath$,
this.getChangeModel().reviewedFiles$,
]),
([path, files]) => {
- this.updateComplete.then(() => {
- assertIsDefined(this.reviewed, 'reviewed');
- this.reviewed.checked = !!path && !!files && files.includes(path);
- });
+ this.reviewed = !!path && !!files && files.includes(path);
}
);
- // When user initially loads the diff view, we want to autmatically mark
+ // When user initially loads the diff view, we want to automatically mark
// the file as reviewed if they have it enabled. We can't observe these
// properties since the method will be called anytime a property updates
// but we only want to call this on the initial load.
subscribe(
this,
() =>
- this.getChangeModel().diffPath$.pipe(
+ this.getViewModel().diffPath$.pipe(
filter(diffPath => !!diffPath),
switchMap(() =>
combineLatest([
this.getChangeModel().patchNum$,
- this.getRouterModel().routerView$,
+ this.getViewModel().childView$,
this.getUserModel().diffPreferences$,
this.getChangeModel().reviewedFiles$,
]).pipe(
filter(
- ([patchNum, routerView, diffPrefs, reviewedFiles]) =>
+ ([patchNum, childView, diffPrefs, reviewedFiles]) =>
!!patchNum &&
- routerView === GerritView.DIFF &&
+ childView === ChangeChildView.DIFF &&
!!diffPrefs &&
!!reviewedFiles
),
@@ -491,14 +497,11 @@
)
),
([patchNum, _routerView, diffPrefs]) => {
- this.setReviewedStatus(patchNum!, diffPrefs);
+ // `patchNum` must be defined, because of the `!!patchNum` filter above.
+ assertIsDefined(patchNum, 'patchNum');
+ this.setReviewedStatus(patchNum, diffPrefs);
}
);
- subscribe(
- this,
- () => this.getChangeModel().diffPath$,
- path => (this.path = path)
- );
}
static override get styles() {
@@ -691,7 +694,6 @@
override connectedCallback() {
super.connectedCallback();
- this.connected$.next(true);
this.throttledToggleFileReviewed = throttleWrap(_ =>
this.handleToggleFileReviewed()
);
@@ -702,38 +704,11 @@
override disconnectedCallback() {
this.cursor?.dispose();
- this.connected$.next(false);
super.disconnectedCallback();
}
- protected override willUpdate(changedProperties: PropertyValues) {
- super.willUpdate(changedProperties);
- if (changedProperties.has('change')) {
- this.allPatchSets = computeAllPatchSets(this.change);
- }
- if (
- changedProperties.has('commentMap') ||
- changedProperties.has('files') ||
- changedProperties.has('path')
- ) {
- this.commentSkips = this.computeCommentSkips(
- this.commentMap,
- this.files?.sortedFileList,
- this.path
- );
- }
-
- if (
- changedProperties.has('changeNum') ||
- changedProperties.has('changeComments') ||
- changedProperties.has('patchRange')
- ) {
- this.fetchFiles();
- }
- }
-
private reInitCursor() {
- assertIsDefined(this.diffHost, 'diffHost');
+ if (!this.diffHost) return;
this.cursor?.replaceDiffs([this.diffHost]);
this.cursor?.reInitCursor();
}
@@ -741,16 +716,35 @@
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
+ changedProperties.has('change') ||
+ changedProperties.has('path') ||
+ changedProperties.has('patchNum') ||
+ changedProperties.has('basePatchNum')
+ ) {
+ this.reloadDiff();
+ } else if (
+ changedProperties.has('isActiveChildView') &&
+ this.isActiveChildView
+ ) {
+ this.initializePositions();
+ }
+ if (
+ changedProperties.has('focusLineNum') ||
+ changedProperties.has('leftSide')
+ ) {
+ this.initLineOfInterestAndCursor();
+ }
+ if (
+ changedProperties.has('change') ||
changedProperties.has('changeComments') ||
changedProperties.has('path') ||
- changedProperties.has('patchRange') ||
+ changedProperties.has('patchNum') ||
+ changedProperties.has('basePatchNum') ||
changedProperties.has('files')
) {
- if (this.changeComments && this.path && this.patchRange) {
+ if (this.change && this.changeComments && this.path && this.patchRange) {
assertIsDefined(this.diffHost, 'diffHost');
- const file = this.files?.changeFilesByPath
- ? this.files.changeFilesByPath[this.path]
- : undefined;
+ const file = this.files?.changeFilesByPath?.[this.path];
this.diffHost.updateComplete.then(() => {
assertIsDefined(this.path);
assertIsDefined(this.patchRange);
@@ -766,17 +760,18 @@
}
override render() {
+ if (!this.isActiveChildView) return nothing;
+ if (!this.patchNum || !this.changeNum || !this.change || !this.path) {
+ return html`<div class="loading">Loading...</div>`;
+ }
const file = this.getFileRange();
return html`
${this.renderStickyHeader()}
- <div class="loading" ?hidden=${!this.loading}>Loading...</div>
<h2 class="assistive-tech-only">Diff view</h2>
<gr-diff-host
id="diffHost"
- ?hidden=${this.loading}
.changeNum=${this.changeNum}
.change=${this.change}
- .commitRange=${this.commitRange}
.patchRange=${this.patchRange}
.file=${file}
.path=${this.path}
@@ -797,7 +792,7 @@
private renderStickyHeader() {
return html` <div
- class="stickyHeader ${this.computeEditMode() ? 'editMode' : ''}"
+ class="stickyHeader ${this.patchNum === EDIT ? 'editMode' : ''}"
>
<h1 class="assistive-tech-only">
Diff of ${this.path ? computeTruncatedPath(this.path) : ''}
@@ -823,7 +818,8 @@
const fileNum = this.computeFileNum(formattedFiles);
const fileNumClass = this.computeFileNumClass(fileNum, formattedFiles);
return html` <div>
- <a href=${this.getChangePath()}>${this.changeNum}</a
+ <a href=${ifDefined(this.getChangeModel().changeUrl())}
+ >${this.changeNum}</a
><span class="changeNumberColon">:</span>
<span class="headerSubject">${this.change?.subject}</span>
<input
@@ -833,6 +829,7 @@
?hidden=${!this.loggedIn}
title="Toggle reviewed status of file"
aria-label="file reviewed"
+ .checked=${this.reviewed}
@change=${this.handleReviewedChange}
/>
<div class="jumpToFileContainer">
@@ -866,7 +863,7 @@
Shortcut.UP_TO_CHANGE,
ShortcutSection.NAVIGATION
)}
- href=${this.getChangePath()}
+ href=${ifDefined(this.getChangeModel().changeUrl())}
>Up</a
>
<span class="separator"></span>
@@ -883,19 +880,10 @@
}
private renderPatchRangeLeft() {
- const revisionInfo = this.change
- ? new RevisionInfoObj(this.change)
- : undefined;
return html` <div class="patchRangeLeft">
<gr-patch-range-select
id="rangeSelect"
- .changeNum=${this.changeNum}
- .patchNum=${this.patchRange?.patchNum}
- .basePatchNum=${this.patchRange?.basePatchNum}
.filesWeblinks=${this.filesWeblinks}
- .availablePatches=${this.allPatchSets}
- .revisions=${this.change?.revisions}
- .revisionInfo=${revisionInfo}
@patch-range-change=${this.handlePatchChange}
>
</gr-patch-range-select>
@@ -1022,7 +1010,7 @@
<gr-download-dialog
id="downloadDialog"
.change=${this.change}
- .patchNum=${this.patchRange?.patchNum}
+ .patchNum=${this.patchNum}
.config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
@@ -1047,36 +1035,12 @@
if (!this.files || !this.path) return;
const fileInfo = this.files.changeFilesByPath[this.path];
const fileRange: FileRange = {path: this.path};
- if (fileInfo && fileInfo.old_path) {
+ if (fileInfo?.old_path) {
fileRange.basePath = fileInfo.old_path;
}
return fileRange;
}
- // Private but used in tests.
- fetchFiles() {
- if (!this.changeNum || !this.patchRange || !this.changeComments) {
- return Promise.resolve();
- }
-
- if (!this.patchRange.patchNum) {
- return Promise.resolve();
- }
-
- return this.restApiService
- .getChangeFiles(this.changeNum, this.patchRange)
- .then(changeFiles => {
- if (!changeFiles) return;
- const commentedPaths = this.changeComments!.getPaths(this.patchRange);
- const files = {...changeFiles};
- addUnmodifiedFiles(files, commentedPaths);
- this.files = {
- sortedFileList: Object.keys(files).sort(specialFilePathCompare),
- changeFilesByPath: files,
- };
- });
- }
-
private handleReviewedChange(e: Event) {
const input = e.target as HTMLInputElement;
this.setReviewed(input.checked ?? false);
@@ -1085,12 +1049,14 @@
// Private but used in tests.
setReviewed(
reviewed: boolean,
- patchNum: RevisionPatchSetNum | undefined = this.patchRange?.patchNum
+ patchNum: RevisionPatchSetNum | undefined = this.patchNum
) {
- if (this.computeEditMode()) return;
+ if (this.patchNum === EDIT) return;
if (!patchNum || !this.path || !this.changeNum) return;
// if file is already reviewed then do not make a saveReview request
if (this.reviewedFiles.has(this.path) && reviewed) return;
+ // optimistic update
+ this.reviewed = reviewed;
this.getChangeModel().setReviewedFilesStatus(
this.changeNum,
patchNum,
@@ -1101,8 +1067,7 @@
// Private but used in tests.
handleToggleFileReviewed() {
- assertIsDefined(this.reviewed);
- this.setReviewed(!this.reviewed.checked);
+ this.setReviewed(!this.reviewed);
}
private handlePrevLine() {
@@ -1147,48 +1112,13 @@
}
// Private but used in tests.
- moveToPreviousFileWithComment() {
- if (!this.commentSkips) return;
- if (!this.change) return;
- if (!this.patchRange?.patchNum) return;
-
- // If there is no previous diff with comments, then return to the change
- // view.
- if (!this.commentSkips.previous) {
- this.navToChangeView();
- return;
+ moveToFileWithComment(direction: -1 | 1) {
+ const path = this.findFileWithComment(direction);
+ if (!path) {
+ this.getChangeModel().navigateToChange();
+ } else {
+ this.getChangeModel().navigateToDiff({path});
}
-
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.commentSkips.previous,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
- );
- }
-
- // Private but used in tests.
- moveToNextFileWithComment() {
- if (!this.commentSkips) return;
- if (!this.change) return;
- if (!this.patchRange?.patchNum) return;
-
- // If there is no next diff with comments, then return to the change view.
- if (!this.commentSkips.next) {
- this.navToChangeView();
- return;
- }
-
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.commentSkips.next,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
- );
}
private handleNewComment() {
@@ -1198,14 +1128,14 @@
private handlePrevFile() {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
- this.navToFile(this.files.sortedFileList, -1);
+ if (!this.files?.sortedPaths) return;
+ this.navToFile(this.files.sortedPaths, -1);
}
private handleNextFile() {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
- this.navToFile(this.files.sortedFileList, 1);
+ if (!this.files?.sortedPaths) return;
+ this.navToFile(this.files.sortedPaths, 1);
}
private handleNextChunk() {
@@ -1249,11 +1179,11 @@
private navigateToUnreviewedFile(direction: string) {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
+ if (!this.files?.sortedPaths) return;
if (!this.reviewedFiles) return;
// Ensure that the currently viewed file always appears in unreviewedFiles
// so we resolve the right "next" file.
- const unreviewedFiles = this.files.sortedFileList.filter(
+ const unreviewedFiles = this.files.sortedPaths.filter(
file => file === this.path || !this.reviewedFiles.has(file)
);
@@ -1277,7 +1207,7 @@
fireEvent(this, 'show-auth-required');
return;
}
- this.navToChangeView(true);
+ this.getChangeModel().navigateToChange(true);
}
private handleToggleLeftPane() {
@@ -1313,10 +1243,6 @@
this.downloadModal.close();
}
- private handleUpToChange() {
- this.navToChangeView();
- }
-
private handleCommaKey() {
if (!this.loggedIn) return;
assertIsDefined(this.diffPreferencesDialog, 'diffPreferencesDialog');
@@ -1336,19 +1262,6 @@
}
// Private but used in tests.
- navToChangeView(openReplyDialog = false) {
- if (!this.changeNum || !this.patchRange?.patchNum) {
- return;
- }
- this.navigateToChange(
- this.change,
- this.patchRange,
- this.change && this.change.revisions,
- openReplyDialog
- );
- }
-
- // Private but used in tests.
navToFile(
fileList: string[],
direction: -1 | 1,
@@ -1356,15 +1269,10 @@
) {
const newPath = this.getNavLinkPath(fileList, direction);
if (!newPath) return;
- if (!this.change) return;
if (!this.patchRange) return;
if (newPath.up) {
- this.navigateToChange(
- this.change,
- this.patchRange,
- this.change && this.change.revisions
- );
+ this.getChangeModel().navigateToChange();
return;
}
@@ -1375,15 +1283,7 @@
newPath.path,
this.patchRange
)?.[0].line;
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: newPath.path,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- lineNum,
- })
- );
+ this.getChangeModel().navigateToDiff({path: newPath.path, lineNum});
}
/**
@@ -1394,35 +1294,25 @@
private computeNavLinkURL(direction?: -1 | 1) {
if (!this.change) return;
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
+ if (!this.files?.sortedPaths) return;
if (!direction) return;
- const newPath = this.getNavLinkPath(this.files.sortedFileList, direction);
- if (!newPath) {
- return;
- }
-
- if (newPath.up) {
- return this.getChangePath();
- }
- return this.getDiffUrl(this.change, this.patchRange, newPath.path);
+ const newPath = this.getNavLinkPath(this.files.sortedPaths, direction);
+ if (!newPath) return;
+ if (newPath.up) return this.getChangeModel().changeUrl();
+ if (!newPath.path) return;
+ return this.getChangeModel().diffUrl({path: newPath.path});
}
private goToEditFile() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ assertIsDefined(this.path, 'path');
// TODO(taoalpha): add a shortcut for editing
const cursorAddress = this.cursor?.getAddress();
- const editUrl = createEditUrl({
- changeNum: this.change._number,
- repo: this.change.project,
+ this.getChangeModel().navigateToEdit({
path: this.path,
- patchNum: this.patchRange.patchNum,
lineNum: cursorAddress?.number,
});
- this.getNavigation().setUrl(editUrl);
}
/**
@@ -1444,7 +1334,6 @@
if (!this.path || !fileList || fileList.length === 0) {
return null;
}
-
let idx = fileList.indexOf(this.path);
if (idx === -1) {
const file = direction > 0 ? fileList[0] : fileList[fileList.length - 1];
@@ -1462,325 +1351,74 @@
}
// Private but used in tests.
- initLineOfInterestAndCursor(leftSide: boolean) {
- assertIsDefined(this.diffHost, 'diffHost');
- this.diffHost.lineOfInterest = this.getLineOfInterest(leftSide);
- this.initCursor(leftSide);
- }
-
- // Private but used in tests.
- displayDiffBaseAgainstLeftToast() {
- if (!this.patchRange) return;
- fireAlert(
- this,
- `Patchset ${this.patchRange.basePatchNum} vs ` +
- `${this.patchRange.patchNum} selected. Press v + \u2190 to view ` +
- `Base vs ${this.patchRange.basePatchNum}`
- );
- }
-
- private displayDiffAgainstLatestToast(latestPatchNum?: PatchSetNum) {
- if (!this.patchRange) return;
- const leftPatchset =
- this.patchRange.basePatchNum === PARENT
- ? 'Base'
- : `Patchset ${this.patchRange.basePatchNum}`;
- fireAlert(
- this,
- `${leftPatchset} vs
- ${this.patchRange.patchNum} selected\n. Press v + \u2191 to view
- ${leftPatchset} vs Patchset ${latestPatchNum}`
- );
- }
-
- private displayToasts() {
- if (!this.patchRange) return;
- if (this.patchRange.basePatchNum !== PARENT) {
- this.displayDiffBaseAgainstLeftToast();
- return;
- }
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (this.patchRange.patchNum !== latestPatchNum) {
- this.displayDiffAgainstLatestToast(latestPatchNum);
- return;
- }
- }
-
- private initCommitRange() {
- let commit: CommitId | undefined;
- let baseCommit: CommitId | undefined;
- if (!this.change) return;
- if (!this.patchRange || !this.patchRange.patchNum) return;
- const revisions = this.change.revisions ?? {};
- for (const [commitSha, revision] of Object.entries(revisions)) {
- const patchNum = revision._number;
- if (patchNum === this.patchRange.patchNum) {
- commit = commitSha as CommitId;
- const commitObj = revision.commit;
- const parents = commitObj?.parents || [];
- if (this.patchRange.basePatchNum === PARENT && parents.length) {
- baseCommit = parents[parents.length - 1].commit;
- }
- } else if (patchNum === this.patchRange.basePatchNum) {
- baseCommit = commitSha as CommitId;
- }
- }
- this.commitRange = commit && baseCommit ? {commit, baseCommit} : undefined;
+ initLineOfInterestAndCursor() {
+ if (!this.diffHost) return;
+ this.diffHost.lineOfInterest = this.getLineOfInterest();
+ this.initCursor();
}
private updateUrlToDiffUrl(lineNum?: number, leftSide?: boolean) {
if (!this.change) return;
- if (!this.patchRange) return;
+ if (!this.patchNum) return;
if (!this.changeNum) return;
if (!this.path) return;
const url = createDiffUrl({
changeNum: this.changeNum,
repo: this.change.project,
- path: this.path,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- lineNum,
- leftSide,
+ patchNum: this.patchNum,
+ basePatchNum: this.basePatchNum,
+ diffView: {
+ path: this.path,
+ lineNum,
+ leftSide,
+ },
});
history.replaceState(null, '', url);
}
- // Private but used in tests.
- initPatchRange() {
- let leftSide = false;
- if (!this.change) return;
- if (this.viewState?.view !== GerritView.DIFF) return;
- if (this.viewState?.commentId) {
- const comment = this.changeComments?.findCommentById(
- this.viewState.commentId
- );
- if (!comment) {
- fireAlert(this, 'comment not found');
- this.getNavigation().setUrl(createChangeUrl({change: this.change}));
- return;
- }
- this.getChangeModel().updatePath(comment.path);
-
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (!latestPatchNum) throw new Error('Missing allPatchSets');
- this.patchRange = getPatchRangeForCommentUrl(comment, latestPatchNum);
- leftSide = isInBaseOfPatchRange(comment, this.patchRange);
-
- this.focusLineNum = comment.line;
- } else {
- if (this.viewState.path) {
- this.getChangeModel().updatePath(this.viewState.path);
- }
- if (this.viewState.patchNum) {
- this.patchRange = {
- patchNum: this.viewState.patchNum,
- basePatchNum: this.viewState.basePatchNum || PARENT,
- };
- }
- if (this.viewState.lineNum) {
- this.focusLineNum = this.viewState.lineNum;
- leftSide = !!this.viewState.leftSide;
- }
- }
- assertIsDefined(this.patchRange, 'patchRange');
- this.initLineOfInterestAndCursor(leftSide);
-
- if (this.viewState?.commentId) {
- // url is of type /comment/{commentId} which isn't meaningful
- this.updateUrlToDiffUrl(this.focusLineNum, leftSide);
- }
-
- this.commentMap = this.getPaths();
+ async reloadDiff() {
+ if (!this.diffHost) return;
+ await this.diffHost.reload(true);
+ this.reporting.diffViewDisplayed();
+ if (this.isBlameLoaded) this.loadBlame();
}
- // Private but used in tests.
- isFileUnchanged(diff?: DiffInfo) {
- if (!diff || !diff.content) return false;
- return !diff.content.some(
- content =>
- (content.a && !content.common) || (content.b && !content.common)
- );
- }
-
- private isSameDiffLoaded(value: DiffViewState) {
- return (
- this.patchRange?.basePatchNum === value.basePatchNum &&
- this.patchRange?.patchNum === value.patchNum &&
- this.path === value.path
- );
- }
-
- private async untilModelLoaded() {
- // NOTE: Wait until this page is connected before determining whether the
- // model is loaded. This can happen when params are changed when setting up
- // this view. It's unclear whether this issue is related to Polymer
- // specifically.
- if (!this.isConnected) {
- await until(this.connected$, connected => connected);
- }
- await until(
- this.getChangeModel().changeLoadingStatus$,
- status => status === LoadingStatus.LOADED
- );
- }
-
- // Private but used in tests.
- viewStateChanged() {
- if (this.viewState === undefined) return;
- const viewState = this.viewState;
-
+ /**
+ * (Re-initialize) the diff view without actually reloading the diff. The
+ * typical user journey is that the user comes back from the change page.
+ */
+ initializePositions() {
// The diff view is kept in the background once created. If the user
// scrolls in the change page, the scrolling is reflected in the diff view
// as well, which means the diff is scrolled to a random position based
// on how much the change view was scrolled.
// Hence, reset the scroll position here.
document.documentElement.scrollTop = 0;
-
- // Everything in the diff view is tied to the change. It seems better to
- // force the re-creation of the diff view when the change number changes.
- const changeChanged = this.changeNum !== viewState.changeNum;
- if (this.changeNum !== undefined && changeChanged) {
- fireEvent(this, EventType.RECREATE_DIFF_VIEW);
- return;
- } else if (
- this.changeNum !== undefined &&
- this.isSameDiffLoaded(viewState)
- ) {
- // changeNum has not changed, so check if there are changes in patchRange
- // path. If no changes then we can simply render the view as is.
- this.reporting.reportInteraction('diff-view-re-rendered');
- // Make sure to re-initialize the cursor because this is typically
- // done on the 'render' event which doesn't fire in this path as
- // rerendering is avoided.
- this.reInitCursor();
- this.diffHost?.initLayers();
- return;
- }
-
- this.files = {sortedFileList: [], changeFilesByPath: {}};
- if (this.isConnected) {
- this.getChangeModel().updatePath(undefined);
- }
- this.patchRange = undefined;
- this.commitRange = undefined;
- this.focusLineNum = undefined;
-
- if (viewState.changeNum && viewState.repo) {
- this.restApiService.setInProjectLookup(
- viewState.changeNum,
- viewState.repo
- );
- }
-
- this.changeNum = viewState.changeNum;
+ this.reInitCursor();
+ this.diffHost?.initLayers();
this.classList.remove('hideComments');
-
- // When navigating away from the page, there is a possibility that the
- // patch number is no longer a part of the URL (say when navigating to
- // the top-level change info view) and therefore undefined in `params`.
- // If route is of type /comment/<commentId>/ then no patchNum is present
- if (!viewState.patchNum && !viewState.commentLink) {
- this.reporting.error(
- 'GrDiffView',
- new Error(`Invalid diff view URL, no patchNum found: ${this.viewState}`)
- );
- return;
- }
-
- const promises: Promise<unknown>[] = [];
- if (!this.change) {
- promises.push(this.untilModelLoaded());
- }
- promises.push(this.waitUntilCommentsLoaded());
-
- if (this.diffHost) {
- this.diffHost.cancel();
- this.diffHost.clearDiffContent();
- }
- this.loading = true;
- return Promise.all(promises)
- .then(() => {
- this.loading = false;
- this.initPatchRange();
- this.initCommitRange();
- return this.updateComplete.then(() => this.diffHost!.reload(true));
- })
- .then(() => {
- this.reporting.diffViewDisplayed();
- })
- .then(() => {
- const fileUnchanged = this.isFileUnchanged(this.diff);
- if (fileUnchanged && viewState.commentLink) {
- assertIsDefined(this.change, 'change');
- assertIsDefined(this.path, 'path');
- assertIsDefined(this.patchRange, 'patchRange');
-
- if (this.patchRange.basePatchNum === PARENT) {
- // file is unchanged between Base vs X
- // hence should not show diff between Base vs Base
- return;
- }
-
- fireAlert(
- this,
- `File is unchanged between Patchset
- ${this.patchRange.basePatchNum} and
- ${this.patchRange.patchNum}. Showing diff of Base vs
- ${this.patchRange.basePatchNum}`
- );
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: this.patchRange.basePatchNum as RevisionPatchSetNum,
- basePatchNum: PARENT,
- lineNum: this.focusLineNum,
- })
- );
- return;
- }
- if (viewState.commentLink) {
- this.displayToasts();
- }
- // If the blame was loaded for a previous file and user navigates to
- // another file, then we load the blame for this file too
- if (this.isBlameLoaded) this.loadBlame();
- });
- }
-
- private async waitUntilCommentsLoaded() {
- await until(this.connected$, c => c);
- await until(this.getCommentsModel().commentsLoading$, isFalse);
}
/**
* If the params specify a diff address then configure the diff cursor.
* Private but used in tests.
*/
- initCursor(leftSide: boolean) {
- if (this.focusLineNum === undefined) {
- return;
- }
+ initCursor() {
+ if (!this.focusLineNum) return;
if (!this.cursor) return;
- if (leftSide) {
- this.cursor.side = Side.LEFT;
- } else {
- this.cursor.side = Side.RIGHT;
- }
+ this.cursor.side = this.leftSide ? Side.LEFT : Side.RIGHT;
this.cursor.initialLineNumber = this.focusLineNum;
}
// Private but used in tests.
- getLineOfInterest(leftSide: boolean): DisplayLine | undefined {
+ getLineOfInterest(): DisplayLine | undefined {
// If there is a line number specified, pass it along to the diff so that
// it will not get collapsed.
- if (!this.focusLineNum) {
- return undefined;
- }
+ if (!this.focusLineNum) return undefined;
return {
lineNum: this.focusLineNum,
- side: leftSide ? Side.LEFT : Side.RIGHT,
+ side: this.leftSide ? Side.LEFT : Side.RIGHT,
};
}
@@ -1790,83 +1428,6 @@
}
}
- private getDiffUrl(
- change?: ChangeInfo | ParsedChangeInfo,
- patchRange?: PatchRange,
- path?: string
- ) {
- if (!change || !patchRange || !path) return '';
- return createDiffUrl({
- changeNum: change._number,
- repo: change.project,
- path,
- patchNum: patchRange.patchNum,
- basePatchNum: patchRange.basePatchNum,
- });
- }
-
- /**
- * When the latest patch of the change is selected (and there is no base
- * patch) then the patch range need not appear in the URL. Return a patch
- * range object with undefined values when a range is not needed.
- */
- private getChangeUrlRange(
- patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo}
- ) {
- let patchNum = undefined;
- let basePatchNum = undefined;
- let latestPatchNum = -1;
- for (const rev of Object.values(revisions || {})) {
- if (typeof rev._number === 'number') {
- latestPatchNum = Math.max(latestPatchNum, rev._number);
- }
- }
- if (!patchRange) return {patchNum, basePatchNum};
- if (
- patchRange.basePatchNum !== PARENT ||
- patchRange.patchNum !== latestPatchNum
- ) {
- patchNum = patchRange.patchNum;
- basePatchNum = patchRange.basePatchNum;
- }
- return {patchNum, basePatchNum};
- }
-
- private getChangePath() {
- if (!this.change) return '';
- if (!this.patchRange) return '';
-
- const range = this.getChangeUrlRange(
- this.patchRange,
- this.change.revisions
- );
- return createChangeUrl({
- change: this.change,
- patchNum: range.patchNum,
- basePatchNum: range.basePatchNum,
- });
- }
-
- // Private but used in tests.
- navigateToChange(
- change?: ChangeInfo | ParsedChangeInfo,
- patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo},
- openReplyDialog?: boolean
- ) {
- if (!change) return;
- const range = this.getChangeUrlRange(patchRange, revisions);
- this.getNavigation().setUrl(
- createChangeUrl({
- change,
- patchNum: range.patchNum,
- basePatchNum: range.basePatchNum,
- openReplyDialog: !!openReplyDialog,
- })
- );
- }
-
// Private but used in tests
formatFilesForDropdown(): DropdownItem[] {
if (!this.files) return [];
@@ -1874,7 +1435,8 @@
if (!this.changeComments) return [];
const dropdownContent: DropdownItem[] = [];
- for (const path of this.files.sortedFileList) {
+ for (const path of this.files.sortedPaths) {
+ const file = this.files.changeFilesByPath[path];
dropdownContent.push({
text: computeDisplayPath(path),
mobileText: computeTruncatedPath(path),
@@ -1882,56 +1444,35 @@
bottomText: this.changeComments.computeCommentsString(
this.patchRange,
path,
- this.files.changeFilesByPath[path],
+ file,
/* includeUnmodified= */ true
),
- file: {...this.files.changeFilesByPath[path], __path: path},
+ file,
});
}
return dropdownContent;
}
// Private but used in tests.
- handleFileChange(e: CustomEvent) {
- if (!this.change) return;
- if (!this.patchRange) return;
-
- // This is when it gets set initially.
- const path = e.detail.value;
- if (path === this.path) {
- return;
- }
-
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
- );
+ handleFileChange(e: ValueChangedEvent<string>) {
+ const path: string = e.detail.value;
+ if (path === this.path) return;
+ this.getChangeModel().navigateToDiff({path});
}
// Private but used in tests.
handlePatchChange(e: CustomEvent) {
- if (!this.change) return;
if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.patchNum) return;
const {basePatchNum, patchNum} = e.detail;
- if (
- basePatchNum === this.patchRange.basePatchNum &&
- patchNum === this.patchRange.patchNum
- ) {
+ if (basePatchNum === this.basePatchNum && patchNum === this.patchNum) {
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum,
- basePatchNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ patchNum,
+ basePatchNum
);
}
@@ -1956,7 +1497,7 @@
computeDownloadDropdownLinks() {
if (!this.change?.project) return [];
if (!this.changeNum) return [];
- if (!this.patchRange?.patchNum) return [];
+ if (!this.patchRange) return [];
if (!this.path) return [];
const links = [
@@ -2004,6 +1545,7 @@
return links;
}
+ // TODO: Move to view-model or router.
// Private but used in tests.
computeDownloadFileLink(
repo: RepoName,
@@ -2032,6 +1574,7 @@
return url;
}
+ // TODO: Move to view-model or router.
// Private but used in tests.
computeDownloadPatchLink(
repo: RepoName,
@@ -2045,49 +1588,19 @@
}
// Private but used in tests.
- getPaths(): CommentMap {
- if (!this.changeComments) return {};
- return this.changeComments.getPaths(this.patchRange);
- }
+ findFileWithComment(direction: -1 | 1): string | undefined {
+ const fileList = this.files?.sortedPaths;
+ const commentMap: CommentMap =
+ this.changeComments?.getPaths(this.patchRange) ?? {};
+ if (!fileList || fileList.length === 0) return undefined;
+ if (!this.path) return undefined;
- // Private but used in tests.
- computeCommentSkips(
- commentMap?: CommentMap,
- fileList?: string[],
- path?: string
- ): CommentSkips | undefined {
- if (!commentMap) return undefined;
- if (!fileList) return undefined;
- if (!path) return undefined;
-
- const skips: CommentSkips = {previous: null, next: null};
- if (!fileList.length) {
- return skips;
+ const pathIndex = fileList.indexOf(this.path);
+ const stopIndex = direction === 1 ? fileList.length : -1;
+ for (let i = pathIndex + direction; i !== stopIndex; i += direction) {
+ if (commentMap[fileList[i]]) return fileList[i];
}
- const pathIndex = fileList.indexOf(path);
-
- // Scan backward for the previous file.
- for (let i = pathIndex - 1; i >= 0; i--) {
- if (commentMap[fileList[i]]) {
- skips.previous = fileList[i];
- break;
- }
- }
-
- // Scan forward for the next file.
- for (let i = pathIndex + 1; i < fileList.length; i++) {
- if (commentMap[fileList[i]]) {
- skips.next = fileList[i];
- break;
- }
- }
-
- return skips;
- }
-
- // Private but used in tests.
- computeEditMode() {
- return this.patchRange?.patchNum === EDIT;
+ return undefined;
}
// Private but used in tests.
@@ -2130,111 +1643,89 @@
// Private but used in tests.
handleDiffAgainstBase() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- if (this.patchRange.basePatchNum === PARENT) {
+ if (this.basePatchNum === PARENT) {
fireAlert(this, 'Base is already selected.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: this.patchRange.patchNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.patchNum,
+ PARENT
);
}
// Private but used in tests.
handleDiffBaseAgainstLeft() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- if (this.patchRange.basePatchNum === PARENT) {
+ if (this.basePatchNum === PARENT) {
fireAlert(this, 'Left is already base.');
return;
}
- const lineNum =
- this.viewState?.view === GerritView.DIFF && this.viewState?.commentLink
- ? this.focusLineNum
- : undefined;
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: this.patchRange.basePatchNum as RevisionPatchSetNum,
- basePatchNum: PARENT,
- lineNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.basePatchNum as RevisionPatchSetNum,
+ PARENT
);
}
// Private but used in tests.
handleDiffAgainstLatest() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (this.patchRange.patchNum === latestPatchNum) {
+ if (this.patchNum === this.latestPatchNum) {
fireAlert(this, 'Latest is already selected.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: latestPatchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.latestPatchNum,
+ this.basePatchNum
);
}
// Private but used in tests.
handleDiffRightAgainstLatest() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (this.patchRange.patchNum === latestPatchNum) {
+ if (this.patchNum === this.latestPatchNum) {
fireAlert(this, 'Right is already latest.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: latestPatchNum,
- basePatchNum: this.patchRange.patchNum as BasePatchSetNum,
- })
+
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.latestPatchNum,
+ this.patchNum as BasePatchSetNum
);
}
// Private but used in tests.
handleDiffBaseAgainstLatest() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (
- this.patchRange.patchNum === latestPatchNum &&
- this.patchRange.basePatchNum === PARENT
- ) {
+ if (this.patchNum === this.latestPatchNum && this.basePatchNum === PARENT) {
fireAlert(this, 'Already diffing base against latest.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: latestPatchNum,
- })
+
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.latestPatchNum,
+ PARENT
);
}
@@ -2265,13 +1756,13 @@
private navigateToNextFileWithCommentThread() {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
- if (!this.patchRange) return;
+ if (!this.files?.sortedPaths) return;
+ const range = this.patchRange;
+ if (!range) return;
if (!this.change) return;
const hasComment = (path: string) =>
- this.changeComments?.getCommentsForPath(path, this.patchRange!)?.length ??
- 0 > 0;
- const filesWithComments = this.files.sortedFileList.filter(
+ this.changeComments?.getCommentsForPath(path, range)?.length ?? 0 > 0;
+ const filesWithComments = this.files.sortedPaths.filter(
file => file === this.path || hasComment(file)
);
this.navToFile(filesWithComments, 1, true);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 6a565eb..9bbe4b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -5,7 +5,6 @@
*/
import '../../../test/common-test-setup';
import './gr-diff-view';
-import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
ChangeStatus,
DiffViewMode,
@@ -18,38 +17,29 @@
query,
queryAll,
queryAndAssert,
- stubReporting,
stubRestApi,
waitEventLoop,
waitUntil,
} from '../../../test/test-utils';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {
- GerritView,
- routerModelToken,
-} from '../../../services/router/router-model';
-import {
createRevisions,
createComment as createCommentGeneric,
- TEST_NUMERIC_CHANGE_ID,
createDiff,
- createPatchRange,
createServerInfo,
createConfig,
createParsedChange,
createRevision,
- createCommit,
createFileInfo,
+ createDiffViewState,
+ TEST_NUMERIC_CHANGE_ID,
} from '../../../test/test-data-generators';
import {
BasePatchSetNum,
CommentInfo,
- CommitId,
EDIT,
- FileInfo,
NumericChangeId,
PARENT,
- PatchRange,
PatchSetNum,
PatchSetNumber,
PathToCommentsInfoMap,
@@ -58,17 +48,15 @@
UrlEncodedCommentId,
} from '../../../types/common';
import {CursorMoveResult} from '../../../api/core';
-import {DiffInfo, Side} from '../../../api/diff';
+import {Side} from '../../../api/diff';
import {Files, GrDiffView} from './gr-diff-view';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon';
+import {SinonFakeTimers, SinonStub} from 'sinon';
import {
changeModelToken,
ChangeModel,
LoadingStatus,
} from '../../../models/change/change-model';
-import {CommentMap} from '../../../utils/comment-util';
-import {ParsedChangeInfo} from '../../../types/types';
import {assertIsDefined} from '../../../utils/common-util';
import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector';
import {fixture, html, assert} from '@open-wc/testing';
@@ -85,6 +73,11 @@
BrowserModel,
browserModelToken,
} from '../../../models/browser/browser-model';
+import {
+ ChangeViewModel,
+ changeViewModelToken,
+} from '../../../models/views/change';
+import {FileNameToNormalizedFileInfoMap} from '../../../models/change/files-model';
function createComment(
id: string,
@@ -107,25 +100,27 @@
let clock: SinonFakeTimers;
let diffCommentsStub;
let getDiffRestApiStub: SinonStub;
- let setUrlStub: SinonStub;
+ let navToChangeStub: SinonStub;
+ let navToDiffStub: SinonStub;
+ let navToEditStub: SinonStub;
let changeModel: ChangeModel;
+ let viewModel: ChangeViewModel;
let commentsModel: CommentsModel;
let browserModel: BrowserModel;
let userModel: UserModel;
function getFilesFromFileList(fileList: string[]): Files {
const changeFilesByPath = fileList.reduce((files, path) => {
- files[path] = createFileInfo();
+ files[path] = createFileInfo(path);
return files;
- }, {} as {[path: string]: FileInfo});
+ }, {} as FileNameToNormalizedFileInfoMap);
return {
- sortedFileList: fileList,
+ sortedPaths: fileList,
changeFilesByPath,
};
}
setup(async () => {
- setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
@@ -144,14 +139,17 @@
stubRestApi('getPortedComments').returns(Promise.resolve({}));
element = await fixture(html`<gr-diff-view></gr-diff-view>`);
- element.changeNum = 42 as NumericChangeId;
+ viewModel = testResolver(changeViewModelToken);
+ viewModel.setState(createDiffViewState());
+ await waitUntil(() => element.changeNum === TEST_NUMERIC_CHANGE_ID);
element.path = 'some/path.txt';
element.change = createParsedChange();
element.diff = {...createDiff(), content: []};
getDiffRestApiStub = stubRestApi('getDiff');
// Delayed in case a test updates element.diff.
getDiffRestApiStub.callsFake(() => Promise.resolve(element.diff));
- element.patchRange = createPatchRange();
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.changeComments = new ChangeComments({
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -163,6 +161,9 @@
changeModel = testResolver(changeModelToken);
browserModel = testResolver(browserModelToken);
userModel = testResolver(userModelToken);
+ navToChangeStub = sinon.stub(changeModel, 'navigateToChange');
+ navToDiffStub = sinon.stub(changeModel, 'navigateToDiff');
+ navToEditStub = sinon.stub(changeModel, 'navigateToEdit');
commentsModel.setState({
comments: {},
@@ -179,279 +180,6 @@
sinon.restore();
});
- test('viewState change triggers diffViewDisplayed()', () => {
- const diffViewDisplayedStub = stubReporting('diffViewDisplayed');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- sinon.stub(element, 'initPatchRange');
- sinon.stub(element, 'fetchFiles');
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.path = '/COMMIT_MSG';
- element.patchRange = createPatchRange();
- return viewStateChangedSpy.returnValues[0]?.then(() => {
- assert.isTrue(diffViewDisplayedStub.calledOnce);
- });
- });
-
- suite('comment route', () => {
- let initLineOfInterestAndCursorStub: SinonStub;
- let replaceStateStub: SinonStub;
- let viewStateChangedSpy: SinonSpy;
- setup(() => {
- initLineOfInterestAndCursorStub = sinon.stub(
- element,
- 'initLineOfInterestAndCursor'
- );
- replaceStateStub = sinon.stub(history, 'replaceState');
- sinon.stub(element, 'fetchFiles');
- stubReporting('diffViewDisplayed');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- });
-
- test('comment url resolves to comment.patch_set vs latest', () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- commentLink: true,
- commentId: 'c1' as UrlEncodedCommentId,
- path: 'abcd',
- patchNum: 1 as RevisionPatchSetNum,
- };
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(11),
- };
- return viewStateChangedSpy.returnValues[0].then(() => {
- assert.isTrue(
- initLineOfInterestAndCursorStub.calledWithExactly(true)
- );
- assert.equal(element.focusLineNum, 10);
- assert.equal(element.patchRange?.patchNum, 11 as RevisionPatchSetNum);
- assert.equal(element.patchRange?.basePatchNum, 2 as BasePatchSetNum);
- assert.isTrue(replaceStateStub.called);
- });
- });
- });
-
- test('viewState change causes blame to load if it was set to true', () => {
- // Blame loads for subsequent files if it was loaded for one file
- element.isBlameLoaded = true;
- stubReporting('diffViewDisplayed');
- const loadBlameStub = sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- sinon.stub(element, 'initPatchRange');
- sinon.stub(element, 'fetchFiles');
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.path = '/COMMIT_MSG';
- element.patchRange = createPatchRange();
- return viewStateChangedSpy.returnValues[0]!.then(() => {
- assert.isTrue(element.isBlameLoaded);
- assert.isTrue(loadBlameStub.calledOnce);
- });
- });
-
- test('unchanged diff X vs latest from comment links navigates to base vs X', async () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- stubReporting('diffViewDisplayed');
- sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- sinon.stub(element, 'isFileUnchanged').returns(true);
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: '/COMMIT_MSG',
- commentLink: true,
- commentId: 'c1' as UrlEncodedCommentId,
- };
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(11),
- };
- await viewStateChangedSpy.returnValues[0];
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/2//COMMIT_MSG#10'
- );
- });
-
- test('unchanged diff Base vs latest from comment does not navigate', async () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- stubReporting('diffViewDisplayed');
- sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- sinon.stub(element, 'isFileUnchanged').returns(true);
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: '/COMMIT_MSG',
- commentLink: true,
- commentId: 'c3' as UrlEncodedCommentId,
- };
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(11),
- };
- await viewStateChangedSpy.returnValues[0];
- assert.isFalse(setUrlStub.calledOnce);
- });
-
- test('isFileUnchanged', () => {
- let diff: DiffInfo = {
- ...createDiff(),
- content: [
- {a: ['abcd'], ab: ['ef']},
- {b: ['ancd'], a: ['xx']},
- ],
- };
- assert.equal(element.isFileUnchanged(diff), false);
- diff = {
- ...createDiff(),
- content: [{ab: ['abcd']}, {ab: ['ancd']}],
- };
- assert.equal(element.isFileUnchanged(diff), true);
- diff = {
- ...createDiff(),
- content: [
- {a: ['abcd'], ab: ['ef'], common: true},
- {b: ['ancd'], ab: ['xx']},
- ],
- };
- assert.equal(element.isFileUnchanged(diff), false);
- diff = {
- ...createDiff(),
- content: [
- {a: ['abcd'], ab: ['ef'], common: true},
- {b: ['ancd'], ab: ['xx'], common: true},
- ],
- };
- assert.equal(element.isFileUnchanged(diff), true);
- });
-
- test('diff toast to go to latest is shown and not base', async () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
-
- stubReporting('diffViewDisplayed');
- sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.change = undefined;
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
- sinon.stub(element, 'isFileUnchanged').returns(false);
- const toastStub = sinon.stub(element, 'displayDiffBaseAgainstLeftToast');
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- repo: 'p' as RepoName,
- commentId: 'c1' as UrlEncodedCommentId,
- commentLink: true,
- };
- await viewStateChangedSpy.returnValues[0];
- assert.isTrue(toastStub.called);
- });
-
test('toggle left diff with a hotkey', () => {
assertIsDefined(element.diffHost);
const toggleLeftDiffStub = sinon.stub(element.diffHost, 'toggleLeftDiff');
@@ -460,20 +188,17 @@
});
test('renders', async () => {
- clock = sinon.useFakeTimers();
- element.changeNum = 42 as NumericChangeId;
browserModel.setScreenWidth(0);
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
- element.change = {
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
+ const change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
+ changeModel.updateStateChange(change);
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
@@ -625,9 +350,8 @@
</a>
</div>
</div>
- <div class="loading">Loading...</div>
<h2 class="assistive-tech-only">Diff view</h2>
- <gr-diff-host hidden="" id="diffHost"> </gr-diff-host>
+ <gr-diff-host id="diffHost"> </gr-diff-host>
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
<gr-diff-preferences-dialog id="diffPreferencesDialog">
</gr-diff-preferences-dialog>
@@ -643,10 +367,8 @@
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
browserModel.setScreenWidth(0);
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -662,51 +384,42 @@
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
- setUrlStub.reset();
+ navToChangeStub.reset();
pressKey(element, 'u');
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
+ assert.isTrue(navToChangeStub.calledOnce);
await element.updateComplete;
pressKey(element, ']');
- assert.equal(setUrlStub.callCount, 2);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/wheatley.md'
- );
+ assert.equal(navToDiffStub.callCount, 1);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: undefined},
+ ]);
+
element.path = 'wheatley.md';
await element.updateComplete;
- assert.isTrue(element.loading);
-
pressKey(element, '[');
- assert.equal(setUrlStub.callCount, 3);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/glados.txt'
- );
+ assert.equal(navToDiffStub.callCount, 2);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'glados.txt', lineNum: undefined},
+ ]);
+
element.path = 'glados.txt';
await element.updateComplete;
- assert.isTrue(element.loading);
-
pressKey(element, '[');
- assert.equal(setUrlStub.callCount, 4);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/chell.go'
- );
+ assert.equal(navToDiffStub.callCount, 3);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'chell.go', lineNum: undefined},
+ ]);
+
element.path = 'chell.go';
await element.updateComplete;
- assert.isTrue(element.loading);
-
pressKey(element, '[');
- assert.equal(setUrlStub.callCount, 5);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
+ assert.equal(navToChangeStub.callCount, 2);
await element.updateComplete;
- assert.isTrue(element.loading);
assertIsDefined(element.diffPreferencesDialog);
const showPrefsStub = sinon
@@ -756,12 +469,8 @@
);
assert.isFalse(element.diffHost.diffElement.displayLine);
- // Note that stubbing setReviewed means that the value of the
- // `element.reviewed` checkbox is not flipped.
const setReviewedStub = sinon.stub(element, 'setReviewed');
const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed');
- assertIsDefined(element.reviewed);
- element.reviewed.checked = false;
assert.isFalse(handleToggleSpy.called);
assert.isFalse(setReviewedStub.called);
@@ -791,10 +500,8 @@
'wheatley.md': [createComment('c2', 21, 10, 'wheatley.md')],
};
element.changeComments = new ChangeComments(comment);
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -810,23 +517,21 @@
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
- setUrlStub.reset();
+ navToDiffStub.reset();
pressKey(element, 'N');
await element.updateComplete;
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/wheatley.md#21'
- );
+ assert.equal(navToDiffStub.callCount, 1);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: 21},
+ ]);
element.path = 'wheatley.md'; // navigated to next file
pressKey(element, 'N');
await element.updateComplete;
- assert.equal(setUrlStub.callCount, 2);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
+ assert.equal(navToChangeStub.callCount, 1);
});
test('shift+x shortcut toggles all diff context', async () => {
@@ -838,114 +543,61 @@
});
test('diff against base', async () => {
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffAgainstBase();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/some/path.txt'
- );
+ const expected = [{path: 'some/path.txt'}, 10, PARENT];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('diff against latest', async () => {
element.path = 'foo';
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(12),
- };
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.latestPatchNum = 12 as PatchSetNumber;
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffAgainstLatest();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..12/foo'
- );
+ const expected = [{path: 'foo'}, 12, 5];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('handleDiffBaseAgainstLeft', async () => {
element.path = 'foo';
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
+ element.latestPatchNum = 10 as PatchSetNumber;
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
+ viewModel.setState({
+ ...createDiffViewState(),
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
- };
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 3 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: 'foo',
- };
+ diffView: {path: 'foo'},
+ });
await element.updateComplete;
element.handleDiffBaseAgainstLeft();
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1/foo');
- });
-
- test('handleDiffBaseAgainstLeft when initially navigating to a comment', () => {
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
- patchNum: 3 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
- sinon.stub(element, 'viewStateChanged');
- element.viewState = {
- commentLink: true,
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- };
- element.focusLineNum = 10;
- element.handleDiffBaseAgainstLeft();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/some/path.txt#10'
- );
+ const expected = [{path: 'foo'}, 1, PARENT];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('handleDiffRightAgainstLatest', async () => {
element.path = 'foo';
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 3 as RevisionPatchSetNum,
- };
+ element.latestPatchNum = 10 as PatchSetNumber;
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffRightAgainstLatest();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/3..10/foo'
- );
+ const expected = [{path: 'foo'}, 10, 3];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('handleDiffBaseAgainstLatest', async () => {
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 3 as RevisionPatchSetNum,
- };
+ element.latestPatchNum = 10 as PatchSetNumber;
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffBaseAgainstLatest();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/some/path.txt'
- );
+ const expected = [{path: 'some/path.txt'}, 10, PARENT];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('A fires an error event when not logged in', async () => {
@@ -954,16 +606,14 @@
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
- assert.isFalse(setUrlStub.calledOnce);
+ assert.isFalse(navToDiffStub.calledOnce);
assert.isTrue(loggedInErrorSpy.called);
});
test('A navigates to change with logged in', async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -976,25 +626,20 @@
await element.updateComplete;
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- setUrlStub.reset();
+ navToDiffStub.reset();
pressKey(element, 'a');
await element.updateComplete;
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10?openReplyDialog=true'
- );
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.deepEqual(navToChangeStub.lastCall.args, [true]);
assert.isFalse(loggedInErrorSpy.called);
});
test('A navigates to change with old patch number with logged in', async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1008,20 +653,15 @@
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1?openReplyDialog=true'
- );
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.deepEqual(navToChangeStub.lastCall.args, [true]);
assert.isFalse(loggedInErrorSpy.called);
});
test('keyboard shortcuts with patch range', () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1038,40 +678,31 @@
element.path = 'glados.txt';
pressKey(element, 'u');
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
+ assert.equal(navToChangeStub.callCount, 1);
pressKey(element, ']');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 2);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10/wheatley.md'
- );
+ assert.equal(navToDiffStub.callCount, 1);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: undefined},
+ ]);
element.path = 'wheatley.md';
pressKey(element, '[');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 3);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10/glados.txt'
- );
+ assert.equal(navToDiffStub.callCount, 2);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'glados.txt', lineNum: undefined},
+ ]);
element.path = 'glados.txt';
pressKey(element, '[');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 4);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10/chell.go'
- );
+ assert.equal(navToDiffStub.callCount, 3);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'chell.go', lineNum: undefined},
+ ]);
element.path = 'chell.go';
pressKey(element, '[');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 5);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
+ assert.equal(navToChangeStub.callCount, 2);
assertIsDefined(element.downloadModal);
const downloadModalStub = sinon.stub(element.downloadModal, 'showModal');
@@ -1079,12 +710,10 @@
assert.isTrue(downloadModalStub.called);
});
- test('keyboard shortcuts with old patch number', () => {
+ test('keyboard shortcuts with old patch number', async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1101,53 +730,57 @@
element.path = 'glados.txt';
pressKey(element, 'u');
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
+ assert.isTrue(navToChangeStub.calledOnce);
pressKey(element, ']');
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/wheatley.md'
- );
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: undefined},
+ ]);
element.path = 'wheatley.md';
pressKey(element, '[');
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/glados.txt'
- );
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'glados.txt', lineNum: undefined},
+ ]);
element.path = 'glados.txt';
pressKey(element, '[');
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/chell.go'
- );
- element.path = 'chell.go';
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'chell.go', lineNum: undefined},
+ ]);
- setUrlStub.reset();
+ element.path = 'chell.go';
+ await element.updateComplete;
+ navToDiffStub.reset();
pressKey(element, '[');
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
+ assert.equal(navToChangeStub.callCount, 2);
+ });
+
+ test('reloadDiff is called when patchNum changes', async () => {
+ const reloadStub = sinon.stub(element, 'reloadDiff');
+ element.patchNum = 5 as RevisionPatchSetNum;
+ await element.updateComplete;
+ assert.isTrue(reloadStub.called);
+ });
+
+ test('initializePositions is called when view becomes active', async () => {
+ const reloadStub = sinon.stub(element, 'reloadDiff');
+ const initializeStub = sinon.stub(element, 'initializePositions');
+
+ element.isActiveChildView = false;
+ await element.updateComplete;
+ element.isActiveChildView = true;
+ await element.updateComplete;
+
+ assert.isTrue(initializeStub.calledOnce);
+ assert.isFalse(reloadStub.called);
});
test('edit should redirect to edit page', async () => {
element.loggedIn = true;
element.path = 't.txt';
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
- element.change = {
- ...createParsedChange(),
- _number: 42 as NumericChangeId,
- project: 'gerrit' as RepoName,
- status: ChangeStatus.NEW,
- revisions: {
- a: createRevision(1),
- b: createRevision(2),
- },
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
await element.updateComplete;
const editBtn = queryAndAssert<GrButton>(
element,
@@ -1155,28 +788,18 @@
);
assert.isTrue(!!editBtn);
editBtn.click();
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/gerrit/+/42/1/t.txt,edit');
+ assert.equal(navToEditStub.callCount, 1);
+ assert.deepEqual(navToEditStub.lastCall.args, [
+ {path: 't.txt', lineNum: undefined},
+ ]);
});
test('edit should redirect to edit page with line number', async () => {
const lineNumber = 42;
element.loggedIn = true;
element.path = 't.txt';
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
- element.change = {
- ...createParsedChange(),
- _number: 42 as NumericChangeId,
- project: 'gerrit' as RepoName,
- status: ChangeStatus.NEW,
- revisions: {
- a: createRevision(1),
- b: createRevision(2),
- },
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
assertIsDefined(element.cursor);
sinon
.stub(element.cursor, 'getAddress')
@@ -1188,11 +811,10 @@
);
assert.isTrue(!!editBtn);
editBtn.click();
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/gerrit/+/42/1/t.txt,edit#42'
- );
+ assert.equal(navToEditStub.callCount, 1);
+ assert.deepEqual(navToEditStub.lastCall.args, [
+ {path: 't.txt', lineNum: 42},
+ ]);
});
async function isEditVisibile({
@@ -1204,10 +826,8 @@
}): Promise<boolean> {
element.loggedIn = loggedIn;
element.path = 't.txt';
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1304,16 +924,10 @@
});
suite('url parameters', () => {
- setup(() => {
- sinon.stub(element, 'fetchFiles');
- });
-
test('_formattedFiles', () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1386,18 +1000,19 @@
});
test('prev/up/next links', async () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
- element.change = {
+ viewModel.setState({
+ ...createDiffViewState(),
+ });
+ const change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
+ changeModel.updateStateChange(change);
+ await element.updateComplete;
+
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
@@ -1417,24 +1032,30 @@
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/wheatley.md'
);
+
element.path = 'wheatley.md';
await element.updateComplete;
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/glados.txt'
);
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(linkEls[2].getAttribute('href'), '/c/test-project/+/42');
+
element.path = 'chell.go';
await element.updateComplete;
+
assert.equal(linkEls[0].getAttribute('href'), '/c/test-project/+/42');
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/glados.txt'
);
+
element.path = 'not_a_real_file';
await element.updateComplete;
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/wheatley.md'
@@ -1447,26 +1068,30 @@
});
test('prev/up/next links with patch range', async () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
+ viewModel.setState({
+ ...createDiffViewState(),
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
- };
- element.change = {
+ diffView: {path: 'glados.txt'},
+ });
+ const change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(5),
b: createRevision(10),
+ c: createRevision(12),
},
};
+ changeModel.updateStateChange(change);
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
- element.path = 'glados.txt';
- await element.updateComplete;
+ await waitUntil(() => element.path === 'glados.txt');
+ await waitUntil(() => element.patchRange?.patchNum === 10);
+
const linkEls = queryAll(element, '.navLink');
assert.equal(linkEls.length, 3);
assert.equal(
@@ -1481,8 +1106,10 @@
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10/wheatley.md'
);
- element.path = 'wheatley.md';
- await element.updateComplete;
+
+ viewModel.updateState({diffView: {path: 'wheatley.md'}});
+ await waitUntil(() => element.path === 'wheatley.md');
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10/glados.txt'
@@ -1495,8 +1122,10 @@
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
- element.path = 'chell.go';
- await element.updateComplete;
+
+ viewModel.updateState({diffView: {path: 'chell.go'}});
+ await waitUntil(() => element.path === 'chell.go');
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10'
@@ -1513,32 +1142,24 @@
});
test('handlePatchChange calls setUrl correctly', async () => {
- element.change = {
- ...createParsedChange(),
- _number: 321 as NumericChangeId,
- project: 'foo/bar' as RepoName,
- };
element.path = 'path/to/file.txt';
-
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 3 as RevisionPatchSetNum,
- };
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
await element.updateComplete;
const detail = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
-
queryAndAssert(element, '#rangeSelect').dispatchEvent(
new CustomEvent('patch-range-change', {detail, bubbles: false})
);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/foo/bar/+/321/1/path/to/file.txt'
- );
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: element.path},
+ detail.patchNum,
+ detail.basePatchNum,
+ ]);
});
test(
@@ -1559,23 +1180,13 @@
manual_review: true,
};
userModel.setDiffPreferences(diffPreferences);
+ viewModel.updateState({diffView: {path: 'wheatley.md'}});
changeModel.setState({
change: createParsedChange(),
- diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- testResolver(routerModelToken).setState({
- changeNum: TEST_NUMERIC_CHANGE_ID,
- view: GerritView.DIFF,
- patchNum: 2 as RevisionPatchSetNum,
- });
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
-
await waitUntil(() => setReviewedStatusStub.called);
assert.isFalse(setReviewedFileStatusStub.called);
@@ -1601,55 +1212,39 @@
manual_review: false,
};
userModel.setDiffPreferences(diffPreferences);
+ viewModel.updateState({diffView: {path: 'wheatley.md'}});
changeModel.setState({
change: createParsedChange(),
- diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- testResolver(routerModelToken).setState({
- changeNum: TEST_NUMERIC_CHANGE_ID,
- view: GerritView.DIFF,
- patchNum: 22 as RevisionPatchSetNum,
- });
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
-
await waitUntil(() => setReviewedFileStatusStub.called);
assert.isTrue(setReviewedFileStatusStub.called);
});
test('file review status', async () => {
+ const saveReviewedStub = sinon
+ .stub(changeModel, 'setReviewedFilesStatus')
+ .callsFake(() => Promise.resolve());
+ userModel.setDiffPreferences(createDefaultDiffPrefs());
+ viewModel.updateState({
+ patchNum: 1 as RevisionPatchSetNum,
+ basePatchNum: PARENT,
+ diffView: {path: '/COMMIT_MSG'},
+ });
changeModel.setState({
change: createParsedChange(),
- diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
element.loggedIn = true;
- const saveReviewedStub = sinon
- .stub(changeModel, 'setReviewedFilesStatus')
- .callsFake(() => Promise.resolve());
+ await waitUntil(() => element.patchRange?.patchNum === 1);
+ await element.updateComplete;
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
- userModel.setDiffPreferences(createDefaultDiffPrefs());
-
- testResolver(routerModelToken).setState({
- changeNum: TEST_NUMERIC_CHANGE_ID,
- view: GerritView.DIFF,
- patchNum: 2 as RevisionPatchSetNum,
- });
-
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
-
await waitUntil(() => saveReviewedStub.called);
changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
@@ -1657,13 +1252,13 @@
const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
element,
- 'input[type="checkbox"]'
+ 'input#reviewed'
);
assert.isTrue(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
- 2,
+ 1,
'/COMMIT_MSG',
true,
]);
@@ -1672,7 +1267,7 @@
assert.isFalse(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
- 2,
+ 1,
'/COMMIT_MSG',
false,
]);
@@ -1684,18 +1279,17 @@
assert.isTrue(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
- 2,
+ 1,
'/COMMIT_MSG',
true,
]);
const callCount = saveReviewedStub.callCount;
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
+ viewModel.setState({
+ ...createDiffViewState(),
repo: 'test' as RepoName,
- };
+ });
await element.updateComplete;
// saveReviewedState observer observes viewState, but should not fire when
@@ -1703,20 +1297,18 @@
assert.equal(saveReviewedStub.callCount, callCount);
});
- test('file review status with edit loaded', async () => {
+ test('do not set file review status for EDIT patchset', async () => {
const saveReviewedStub = sinon.stub(
changeModel,
'setReviewedFilesStatus'
);
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: EDIT,
- };
+ element.patchNum = EDIT;
+ element.basePatchNum = 1 as BasePatchSetNum;
await waitEventLoop();
- assert.isTrue(element.computeEditMode());
element.setReviewed(true);
+
assert.isFalse(saveReviewedStub.called);
});
@@ -1725,14 +1317,7 @@
sinon.stub(element.diffHost, 'reload');
const initLineStub = sinon.stub(element, 'initLineOfInterestAndCursor');
- element.loggedIn = true;
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
+ element.focusLineNum = 123;
await element.updateComplete;
await waitEventLoop();
@@ -1780,115 +1365,56 @@
assert.isTrue(diffModeSelector.classList.contains('hide'));
});
- suite('commitRange', () => {
- const change: ParsedChangeInfo = {
- ...createParsedChange(),
- _number: 42 as NumericChangeId,
- revisions: {
- 'commit-sha-1': {
- ...createRevision(1),
- commit: {
- ...createCommit(),
- parents: [{subject: 's1', commit: 'sha-1-parent' as CommitId}],
- },
- },
- 'commit-sha-2': createRevision(2),
- 'commit-sha-3': createRevision(3),
- 'commit-sha-4': createRevision(4),
- 'commit-sha-5': {
- ...createRevision(5),
- commit: {
- ...createCommit(),
- parents: [{subject: 's5', commit: 'sha-5-parent' as CommitId}],
- },
- },
- },
- };
- setup(async () => {
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload');
- sinon.stub(element, 'initCursor');
- element.change = change;
- await element.updateComplete;
- await element.diffHost.updateComplete;
- });
-
- test('uses the patchNum and basePatchNum ', async () => {
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 4 as RevisionPatchSetNum,
- basePatchNum: 2 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.change = change;
- await element.updateComplete;
- await waitEventLoop();
- assert.deepEqual(element.commitRange, {
- baseCommit: 'commit-sha-2' as CommitId,
- commit: 'commit-sha-4' as CommitId,
- });
- });
-
- test('uses the parent when there is no base patch num ', async () => {
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 5 as RevisionPatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.change = change;
- await element.updateComplete;
- await waitEventLoop();
- assert.deepEqual(element.commitRange, {
- commit: 'commit-sha-5' as CommitId,
- baseCommit: 'sha-5-parent' as CommitId,
- });
- });
- });
-
test('initCursor', () => {
assertIsDefined(element.cursor);
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when viewState specify no cursor address:
- element.initCursor(false);
+ element.leftSide = false;
+ element.initCursor();
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when viewState specify side but no number:
- element.initCursor(true);
+ element.leftSide = true;
+ element.initCursor();
assert.isNotOk(element.cursor.initialLineNumber);
// Revision hash: specifies lineNum but not side.
element.focusLineNum = 234;
- element.initCursor(false);
+ element.leftSide = false;
+ element.initCursor();
assert.equal(element.cursor.initialLineNumber, 234);
assert.equal(element.cursor.side, Side.RIGHT);
// Base hash: specifies lineNum and side.
element.focusLineNum = 345;
- element.initCursor(true);
+ element.leftSide = true;
+ element.initCursor();
assert.equal(element.cursor.initialLineNumber, 345);
assert.equal(element.cursor.side, Side.LEFT);
// Specifies right side:
element.focusLineNum = 123;
- element.initCursor(false);
+ element.leftSide = false;
+ element.initCursor();
assert.equal(element.cursor.initialLineNumber, 123);
assert.equal(element.cursor.side, Side.RIGHT);
});
test('getLineOfInterest', () => {
- assert.isUndefined(element.getLineOfInterest(false));
+ element.leftSide = false;
+ assert.isUndefined(element.getLineOfInterest());
element.focusLineNum = 12;
- let result = element.getLineOfInterest(false);
+ element.leftSide = false;
+ let result = element.getLineOfInterest();
assert.isOk(result);
assert.equal(result!.lineNum, 12);
assert.equal(result!.side, Side.RIGHT);
- result = element.getLineOfInterest(true);
+ element.leftSide = true;
+ result = element.getLineOfInterest();
assert.isOk(result);
assert.equal(result!.lineNum, 12);
assert.equal(result!.side, Side.LEFT);
@@ -1907,10 +1433,8 @@
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
- element.patchRange = {
- basePatchNum: 3 as BasePatchSetNum,
- patchNum: 5 as RevisionPatchSetNum,
- };
+ element.patchNum = 5 as RevisionPatchSetNum;
+ element.basePatchNum = 3 as BasePatchSetNum;
const e = {detail: {number: 123, side: Side.RIGHT}} as CustomEvent;
element.onLineSelected(e);
@@ -1931,10 +1455,8 @@
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
- element.patchRange = {
- basePatchNum: 3 as BasePatchSetNum,
- patchNum: 5 as RevisionPatchSetNum,
- };
+ element.patchNum = 5 as RevisionPatchSetNum;
+ element.basePatchNum = 3 as BasePatchSetNum;
const e = {detail: {number: 123, side: Side.LEFT}} as CustomEvent;
element.onLineSelected(e);
@@ -1965,199 +1487,116 @@
});
});
- suite('initPatchRange', () => {
- setup(async () => {
- getDiffRestApiStub.returns(Promise.resolve(createDiff()));
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 3 as RevisionPatchSetNum,
- path: 'abcd',
- };
- await element.updateComplete;
- });
- test('empty', () => {
- sinon.stub(element, 'getPaths').returns({});
- element.initPatchRange();
- assert.equal(Object.keys(element.commentMap ?? {}).length, 0);
- });
-
- test('has paths', () => {
- sinon.stub(element, 'fetchFiles');
- sinon.stub(element, 'getPaths').returns({
- 'path/to/file/one.cpp': true,
- 'path-to/file/two.py': true,
- });
- element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: 3 as BasePatchSetNum,
- patchNum: 5 as RevisionPatchSetNum,
- };
- element.initPatchRange();
- assert.deepEqual(Object.keys(element.commentMap ?? {}), [
- 'path/to/file/one.cpp',
- 'path-to/file/two.py',
- ]);
- });
- });
-
- suite('computeCommentSkips', () => {
+ suite('findFileWithComment', () => {
test('empty file list', () => {
- const commentMap = {
- 'path/one.jpg': true,
- 'path/three.wav': true,
- };
- const path = 'path/two.m4v';
- const result = element.computeCommentSkips(commentMap, [], path);
- assert.isOk(result);
- assert.isNotOk(result!.previous);
- assert.isNotOk(result!.next);
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = 'path/two.m4v';
+ assert.isUndefined(element.findFileWithComment(-1));
+ assert.isUndefined(element.findFileWithComment(1));
});
test('finds skips', () => {
const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
- let path = fileList[1];
- const commentMap: CommentMap = {};
- commentMap[fileList[0]] = true;
- commentMap[fileList[1]] = false;
- commentMap[fileList[2]] = true;
+ element.files = {sortedPaths: fileList, changeFilesByPath: {}};
+ element.path = fileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
- let result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.equal(result!.previous, fileList[0]);
- assert.equal(result!.next, fileList[2]);
+ assert.equal(element.findFileWithComment(-1), fileList[0]);
+ assert.equal(element.findFileWithComment(1), fileList[2]);
- commentMap[fileList[1]] = true;
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/two.m4v': [createComment('c1', 1, 1, 'path/two.m4v')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
- result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.equal(result!.previous, fileList[0]);
- assert.equal(result!.next, fileList[2]);
+ assert.equal(element.findFileWithComment(-1), fileList[0]);
+ assert.equal(element.findFileWithComment(1), fileList[2]);
- path = fileList[0];
+ element.path = fileList[0];
- result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.isNull(result!.previous);
- assert.equal(result!.next, fileList[1]);
+ assert.isUndefined(element.findFileWithComment(-1));
+ assert.equal(element.findFileWithComment(1), fileList[1]);
- path = fileList[2];
+ element.path = fileList[2];
- result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.equal(result!.previous, fileList[1]);
- assert.isNull(result!.next);
+ assert.equal(element.findFileWithComment(-1), fileList[1]);
+ assert.isUndefined(element.findFileWithComment(1));
});
suite('skip next/previous', () => {
- let navToChangeStub: SinonStub;
-
setup(() => {
- navToChangeStub = sinon.stub(element, 'navToChangeView');
element.files = getFilesFromFileList([
'path/one.jpg',
'path/two.m4v',
'path/three.wav',
]);
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
+ element.patchNum = 2 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
});
- suite('moveToPreviousFileWithComment', () => {
- test('no skips', () => {
- element.moveToPreviousFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(setUrlStub.called);
- });
-
+ suite('moveToFileWithComment previous', () => {
test('no previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = false;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = true;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToPreviousFileWithComment();
+ element.moveToFileWithComment(-1);
assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(setUrlStub.called);
+ assert.isFalse(navToDiffStub.called);
});
test('w/ previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = true;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = true;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToPreviousFileWithComment();
+ element.moveToFileWithComment(-1);
assert.isFalse(navToChangeStub.called);
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
});
});
- suite('moveToNextFileWithComment', () => {
- test('no skips', () => {
- element.moveToNextFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(setUrlStub.called);
- });
-
+ suite('moveToFileWithComment next', () => {
test('no previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = true;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = false;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToNextFileWithComment();
+ element.moveToFileWithComment(1);
assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(setUrlStub.called);
+ assert.isFalse(navToDiffStub.called);
});
test('w/ previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = true;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = true;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToNextFileWithComment();
+ element.moveToFileWithComment(1);
assert.isFalse(navToChangeStub.called);
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
});
});
});
});
- test('_computeEditMode', () => {
- const callCompute = (range: PatchRange) => {
- element.patchRange = range;
- return element.computeEditMode();
- };
- assert.isFalse(
- callCompute({
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- })
- );
- assert.isTrue(
- callCompute({
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: EDIT,
- })
- );
- });
-
test('computeFileNum', () => {
element.path = '/foo';
assert.equal(
@@ -2224,15 +1663,18 @@
test('reviewed checkbox', async () => {
sinon.stub(element, 'handlePatchChange');
- element.patchRange = createPatchRange();
- await element.updateComplete;
- assertIsDefined(element.reviewed);
- // Reviewed checkbox should be shown.
- assert.isTrue(isVisible(element.reviewed));
- element.patchRange = {...element.patchRange, patchNum: EDIT};
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
await element.updateComplete;
- assert.isFalse(isVisible(element.reviewed));
+ let checkbox = queryAndAssert(element, '#reviewed');
+ assert.isTrue(isVisible(checkbox));
+
+ element.patchNum = EDIT;
+ await element.updateComplete;
+
+ checkbox = queryAndAssert(element, '#reviewed');
+ assert.isFalse(isVisible(checkbox));
});
});
@@ -2380,45 +1822,40 @@
sinon.stub(element, 'initLineOfInterestAndCursor');
// Load file1
- element.viewState = {
- view: GerritView.DIFF,
+ viewModel.setState({
+ ...createDiffViewState(),
patchNum: 1 as RevisionPatchSetNum,
- changeNum: 101 as NumericChangeId,
repo: 'test-project' as RepoName,
- path: 'file1',
- };
- element.patchRange = {
- patchNum: 1 as RevisionPatchSetNum,
- basePatchNum: PARENT,
- };
+ diffView: {path: 'file1'},
+ });
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
revisions: createRevisions(1),
};
await element.updateComplete;
- assert.isFalse(setUrlStub.called);
+ assert.isFalse(navToDiffStub.called);
// Switch to file2
element.handleFileChange(
new CustomEvent('value-change', {detail: {value: 'file2'}})
);
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
+ assert.deepEqual(navToDiffStub.lastCall.firstArg, {path: 'file2'});
// This is to mock the param change triggered by above navigate
- element.viewState = {
- view: GerritView.DIFF,
+ viewModel.setState({
+ ...createDiffViewState(),
patchNum: 1 as RevisionPatchSetNum,
- changeNum: 101 as NumericChangeId,
repo: 'test-project' as RepoName,
- path: 'file2',
- };
- element.patchRange = {
- patchNum: 1 as RevisionPatchSetNum,
- basePatchNum: PARENT,
- };
+ diffView: {path: 'file2'},
+ });
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
// No extra call
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
});
test('_computeDownloadDropdownLinks', () => {
@@ -2440,10 +1877,8 @@
element.change = createParsedChange();
element.change.project = 'test' as RepoName;
element.changeNum = 12 as NumericChangeId;
- element.patchRange = {
- patchNum: 1 as RevisionPatchSetNum,
- basePatchNum: PARENT,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.path = 'index.php';
element.diff = createDiff();
assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks);
@@ -2472,10 +1907,8 @@
element.change = createParsedChange();
element.change.project = 'test' as RepoName;
element.changeNum = 12 as NumericChangeId;
- element.patchRange = {
- patchNum: 3 as RevisionPatchSetNum,
- basePatchNum: 2 as BasePatchSetNum,
- };
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 2 as BasePatchSetNum;
element.path = 'index.php';
element.diff = diff;
assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks);
@@ -2539,49 +1972,4 @@
);
});
});
-
- suite('unmodified files with comments', () => {
- let element: GrDiffView;
-
- setup(async () => {
- const changedFiles = {
- 'file1.txt': createFileInfo(),
- 'a/b/test.c': createFileInfo(),
- };
- stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
- stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
- stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
- stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response()));
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
- stubRestApi('getReviewedFiles').returns(Promise.resolve([]));
- element = await fixture(html`<gr-diff-view></gr-diff-view>`);
- element.changeNum = 42 as NumericChangeId;
- });
-
- test('fetchFiles add files with comments without changes', () => {
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
- element.changeComments = {
- getPaths: sinon.stub().returns({
- 'file2.txt': {},
- 'file1.txt': {},
- }),
- } as unknown as ChangeComments;
- element.changeNum = 23 as NumericChangeId;
- return element.fetchFiles().then(() => {
- assert.deepEqual(element.files, {
- sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
- changeFilesByPath: {
- 'file1.txt': createFileInfo(),
- 'file2.txt': {status: 'U'} as FileInfo,
- 'a/b/test.c': createFileInfo(),
- },
- });
- });
- });
- });
});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index d9f88f7..325798c 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -21,6 +21,7 @@
import {
BasePatchSetNum,
EDIT,
+ NumericChangeId,
PARENT,
PatchSetNum,
RevisionInfo,
@@ -36,7 +37,7 @@
import {EditRevisionInfo} from '../../../types/types';
import {a11yStyles} from '../../../styles/gr-a11y-styles';
import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {LitElement, css, html, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
import {commentsModelToken} from '../../../models/comments/comments-model';
@@ -44,6 +45,8 @@
import {ifDefined} from 'lit/directives/if-defined.js';
import {ValueChangedEvent} from '../../../types/events';
import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {changeModelToken} from '../../../models/change/change-model';
+import {changeViewModelToken} from '../../../models/views/change';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -83,33 +86,27 @@
@query('#patchNumDropdown')
patchNumDropdown?: GrDropdownList;
- @property({type: Array})
- availablePatches?: PatchSet[];
+ @state()
+ availablePatches: PatchSet[] = [];
- @property({type: String})
- changeNum?: string;
+ @state()
+ changeNum?: NumericChangeId;
@property({type: Object})
filesWeblinks?: FilesWebLinks;
- @property({type: String})
+ @state()
patchNum?: RevisionPatchSetNum;
- @property({type: String})
+ @state()
basePatchNum?: BasePatchSetNum;
- /** Not used directly. Translated into `sortedRevisions` in willUpdate(). */
- @property({type: Object})
- revisions: (RevisionInfo | EditRevisionInfo)[] = [];
-
- @property({type: Object})
+ @state()
revisionInfo?: RevisionInfoClass;
- /** Private internal state, derived from `revisions` in willUpdate(). */
@state()
- private sortedRevisions: (RevisionInfo | EditRevisionInfo)[] = [];
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[] = [];
- /** Private internal state, visible for testing. */
@state()
changeComments?: ChangeComments;
@@ -118,10 +115,44 @@
private readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getViewModel = resolve(this, changeViewModelToken);
+
constructor() {
super();
subscribe(
this,
+ () => this.getViewModel().changeNum$,
+ x => (this.changeNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().change$,
+ x => (this.revisionInfo = x ? new RevisionInfoClass(x) : undefined)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().patchNum$,
+ x => (this.patchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().basePatchNum$,
+ x => (this.basePatchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().patchsets$,
+ x => (this.availablePatches = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().revisions$,
+ x => (this.sortedRevisions = sortRevisions(Object.values(x || {})))
+ );
+ subscribe(
+ this,
() => this.getCommentsModel().changeComments$,
x => (this.changeComments = x)
);
@@ -164,6 +195,9 @@
}
override render() {
+ if (!this.changeNum || !this.patchNum || !this.basePatchNum) {
+ return nothing;
+ }
return html`
<h3 class="assistive-tech-only">Patchset Range Selection</h3>
<span class="patchRange" aria-label="patch range starts with">
@@ -203,16 +237,9 @@
> `;
}
- override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('revisions')) {
- this.sortedRevisions = sortRevisions(Object.values(this.revisions || {}));
- }
- }
-
// Private method, but visible for testing.
computeBaseDropdownContent(): DropdownItem[] {
if (
- this.availablePatches === undefined ||
this.patchNum === undefined ||
this.changeComments === undefined ||
this.revisionInfo === undefined
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index c90992f..584fcd7 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -9,7 +9,7 @@
import {GrPatchRangeSelect} from './gr-patch-range-select';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
-import {stubReporting, stubRestApi} from '../../../test/test-utils';
+import {stubReporting} from '../../../test/test-utils';
import {
BasePatchSetNum,
EDIT,
@@ -25,8 +25,11 @@
import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
import {SpecialFilePath} from '../../../constants/constants';
import {
+ createChangeViewState,
createEditRevision,
+ createParsedChange,
createRevision,
+ createRevisions,
} from '../../../test/test-data-generators';
import {PatchSet} from '../../../utils/patch-set-util';
import {
@@ -36,6 +39,9 @@
import {queryAndAssert} from '../../../test/test-utils';
import {fire} from '../../../utils/event-util';
import {fixture, html, assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
+import {changeViewModelToken} from '../../../models/views/change';
+import {changeModelToken} from '../../../models/change/change-model';
type RevIdToRevisionInfo = {
[revisionId: string]: RevisionInfo | EditRevisionInfo;
@@ -53,16 +59,23 @@
}
setup(async () => {
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
-
// Element must be wrapped in an element with direct access to the
// comment API.
element = await fixture(
html`<gr-patch-range-select></gr-patch-range-select>`
);
+ const viewModel = testResolver(changeViewModelToken);
+ viewModel.setState({
+ ...createChangeViewState(),
+ patchNum: 1 as RevisionPatchSetNum,
+ basePatchNum: PARENT,
+ });
+ const changeModel = testResolver(changeModelToken);
+ changeModel.updateStateChange({
+ ...createParsedChange(),
+ revisions: createRevisions(5),
+ });
// Stub methods on the changeComments object after changeComments has
// been initialized.
element.changeComments = new ChangeComments();
@@ -86,7 +99,7 @@
});
test('enabled/disabled options', async () => {
- element.revisions = [
+ element.sortedRevisions = [
createRevision(3),
createEditRevision(2),
createRevision(2),
@@ -119,13 +132,13 @@
{num: 2, sha: '3'} as PatchSet,
{num: 1, sha: '4'} as PatchSet,
];
- element.revisions = [
- createRevision(2),
- createRevision(3),
- createRevision(1),
+ element.sortedRevisions = [
createRevision(4),
+ createRevision(3),
+ createRevision(2),
+ createRevision(1),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
const expectedResult: DropdownItem[] = [
{
disabled: true,
@@ -175,13 +188,13 @@
});
test('computeBaseDropdownContent called when patchNum updates', async () => {
- element.revisions = [
- createRevision(2),
- createRevision(3),
- createRevision(1),
+ element.sortedRevisions = [
createRevision(4),
+ createRevision(3),
+ createRevision(2),
+ createRevision(1),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.availablePatches = [
{num: 1, sha: '1'} as PatchSet,
{num: 2, sha: '2'} as PatchSet,
@@ -201,13 +214,13 @@
});
test('computeBaseDropdownContent called when changeComments update', async () => {
- element.revisions = [
- createRevision(2),
- createRevision(3),
- createRevision(1),
+ element.sortedRevisions = [
createRevision(4),
+ createRevision(3),
+ createRevision(2),
+ createRevision(1),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.availablePatches = [
{num: 3, sha: '2'} as PatchSet,
{num: 2, sha: '3'} as PatchSet,
@@ -226,13 +239,13 @@
});
test('computePatchDropdownContent called when basePatchNum updates', async () => {
- element.revisions = [
+ element.sortedRevisions = [
createRevision(2),
createRevision(3),
createRevision(1),
createRevision(4),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.availablePatches = [
{num: 1, sha: '1'} as PatchSet,
{num: 2, sha: '2'} as PatchSet,
@@ -258,7 +271,7 @@
{num: 1, sha: '4'} as PatchSet,
];
element.basePatchNum = 1 as BasePatchSetNum;
- element.revisions = [
+ element.sortedRevisions = [
createRevision(3),
createEditRevision(2),
createRevision(2, 'description'),
@@ -402,13 +415,13 @@
{num: 2, sha: '3'} as PatchSet,
{num: 1, sha: '4'} as PatchSet,
];
- element.revisions = [
+ element.sortedRevisions = [
createRevision(2),
createRevision(3),
createRevision(1),
createRevision(4),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
await element.updateComplete;
element.addEventListener('patch-range-change', handler);
@@ -444,13 +457,13 @@
{num: 2, sha: '3'} as PatchSet,
{num: 1, sha: '4'} as PatchSet,
];
- element.revisions = [
+ element.sortedRevisions = [
createRevision(2),
createRevision(3),
createRevision(1),
createRevision(4),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.patchNum = 1 as PatchSetNumber;
element.basePatchNum = PARENT;
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index d1d05b7..ec1e48e 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -29,7 +29,7 @@
import {customElement, property, query, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {IronInputElement} from '@polymer/iron-input/iron-input';
-import {createEditUrl} from '../../../models/views/edit';
+import {createEditUrl} from '../../../models/views/change';
import {resolve} from '../../../models/dependency';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {whenVisible} from '../../../utils/dom-util';
@@ -429,8 +429,8 @@
const url = createEditUrl({
changeNum: this.change._number,
repo: this.change.project,
- path: this.path,
patchNum: this.patchNum,
+ editView: {path: this.path},
});
this.getNavigation().setUrl(url);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 8e346c3..acc4c9e 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -31,8 +31,12 @@
import {resolve} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
import {ShortcutController} from '../../lit/shortcut-controller';
-import {editViewModelToken, EditViewState} from '../../../models/views/edit';
-import {createChangeUrl} from '../../../models/views/change';
+import {
+ ChangeChildView,
+ changeViewModelToken,
+ ChangeViewState,
+ createChangeUrl,
+} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
@@ -60,7 +64,7 @@
*/
@property({type: Object})
- viewState?: EditViewState;
+ viewState?: ChangeViewState;
// private but used in test
@state() change?: ParsedChangeInfo;
@@ -95,7 +99,7 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly getEditViewModel = resolve(this, editViewModelToken);
+ private readonly getViewModel = resolve(this, changeViewModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -116,8 +120,10 @@
);
subscribe(
this,
- () => this.getEditViewModel().state$,
+ () => this.getViewModel().state$,
state => {
+ // TODO: Add a setter for `viewState` instead of relying on the
+ // `viewStateChanged()` call here.
this.viewState = state;
this.viewStateChanged();
}
@@ -206,7 +212,7 @@
}
override render() {
- if (!this.viewState) return;
+ if (this.viewState?.childView !== ChangeChildView.EDIT) return nothing;
return html` ${this.renderHeader()} ${this.renderEndpoint()} `;
}
@@ -220,7 +226,7 @@
<span class="separator"></span>
<gr-editable-label
labelText="File path"
- .value=${this.viewState?.path}
+ .value=${this.viewState?.editView?.path}
placeholder="File path..."
@changed=${this.handlePathChanged}
></gr-editable-label>
@@ -277,7 +283,7 @@
></gr-endpoint-param>
<gr-endpoint-param
name="lineNum"
- .value=${this.viewState?.lineNum}
+ .value=${this.viewState?.editView?.lineNum}
></gr-endpoint-param>
<gr-default-editor
id="file"
@@ -298,19 +304,21 @@
}
get storageKey() {
- return `c${this.viewState?.changeNum}_ps${this.viewState?.patchNum}_${this.viewState?.path}`;
+ return `c${this.viewState?.changeNum}_ps${this.viewState?.patchNum}_${this.viewState?.editView?.path}`;
}
// private but used in test
viewStateChanged() {
- if (!this.viewState) return;
+ if (this.viewState?.childView !== ChangeChildView.EDIT) return;
// NOTE: This may be called before attachment (e.g. while parentElement is
// null). Fire title-change in an async so that, if attachment to the DOM
// has been queued, the event can bubble up to the handler in gr-app.
setTimeout(() => {
if (!this.viewState) return;
- const title = `Editing ${computeTruncatedPath(this.viewState.path)}`;
+ const title = `Editing ${computeTruncatedPath(
+ this.viewState.editView?.path
+ )}`;
fireTitleChange(this, title);
});
@@ -347,7 +355,7 @@
// private but used in test
async handlePathChanged(e: CustomEvent<string>): Promise<void> {
const changeNum = this.viewState?.changeNum;
- const currentPath = this.viewState?.path;
+ const currentPath = this.viewState?.editView?.path;
assertIsDefined(changeNum, 'change number');
assertIsDefined(currentPath, 'path');
@@ -376,7 +384,7 @@
getFileData() {
const changeNum = this.viewState?.changeNum;
const patchNum = this.viewState?.patchNum;
- const path = this.viewState?.path;
+ const path = this.viewState?.editView?.path;
assertIsDefined(changeNum, 'change number');
assertIsDefined(patchNum, 'patchset number');
assertIsDefined(path, 'path');
@@ -416,7 +424,7 @@
// private but used in test
saveEdit() {
const changeNum = this.viewState?.changeNum;
- const path = this.viewState?.path;
+ const path = this.viewState?.editView?.path;
assertIsDefined(changeNum, 'change number');
assertIsDefined(path, 'path');
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index d428e18..1a4879d 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -72,7 +72,7 @@
labeltext="File path"
placeholder="File path..."
tabindex="0"
- title="${element.viewState?.path}"
+ title="${element.viewState?.editView?.path}"
>
</gr-editable-label>
</span>
@@ -373,7 +373,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
// Ensure no data is set with a bad response.
@@ -392,7 +392,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
// Ensure no data is set with a bad response.
@@ -415,7 +415,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
return element.getFileData().then(() => {
@@ -433,7 +433,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
return element.getFileData().then(() => {
@@ -530,7 +530,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: 1 as RevisionPatchSetNum,
- path: 'test',
+ editView: {path: 'test'},
};
const alertStub = sinon.stub();
@@ -562,7 +562,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: 1 as RevisionPatchSetNum,
- path: 'test',
+ editView: {path: 'test'},
};
const alertStub = sinon.stub();
@@ -583,7 +583,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: 1 as RevisionPatchSetNum,
- path: 'test',
+ editView: {path: 'test'},
};
assert.equal(element.storageKey, 'c1_ps1_test');
});
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index e959e05..eceb05e 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -74,6 +74,7 @@
import {userModelToken} from '../models/user/user-model';
import {modalStyles} from '../styles/gr-modal-styles';
import {AdminChildView, createAdminUrl} from '../models/views/admin';
+import {ChangeChildView, changeViewModelToken} from '../models/views/change';
interface ErrorInfo {
text: string;
@@ -119,6 +120,9 @@
@state() private view?: GerritView;
+ // TODO: Introduce a wrapper element for CHANGE, DIFF, EDIT view.
+ @state() private childView?: ChangeChildView;
+
@state() private lastError?: ErrorInfo;
// private but used in test
@@ -168,6 +172,8 @@
private readonly getRouterModel = resolve(this, routerModelToken);
+ private readonly getChangeViewModel = resolve(this, changeViewModelToken);
+
constructor() {
super();
@@ -237,6 +243,13 @@
if (view) this.errorView?.classList.remove('show');
}
);
+ subscribe(
+ this,
+ () => this.getChangeViewModel().childView$,
+ childView => {
+ this.childView = childView;
+ }
+ );
prefersDarkColorScheme().addEventListener('change', () => {
if (this.theme === AppTheme.AUTO) {
@@ -464,9 +477,7 @@
this.updateComplete.then(() => (this.invalidateChangeViewCache = false));
return nothing;
}
- return cache(
- this.view === GerritView.CHANGE ? this.changeViewTemplate() : nothing
- );
+ return cache(this.isChangeView() ? this.changeViewTemplate() : nothing);
}
// Template as not to create duplicates, for renderChangeView() only.
@@ -476,8 +487,27 @@
`;
}
+ private isChangeView() {
+ return (
+ this.view === GerritView.CHANGE &&
+ this.childView === ChangeChildView.OVERVIEW
+ );
+ }
+
+ private isDiffView() {
+ return (
+ this.view === GerritView.CHANGE && this.childView === ChangeChildView.DIFF
+ );
+ }
+
+ private isEditorView() {
+ return (
+ this.view === GerritView.CHANGE && this.childView === ChangeChildView.EDIT
+ );
+ }
+
private renderEditorView() {
- if (this.view !== GerritView.EDIT) return nothing;
+ if (!this.isEditorView()) return nothing;
return html`<gr-editor-view></gr-editor-view>`;
}
@@ -486,9 +516,7 @@
this.updateComplete.then(() => (this.invalidateDiffViewCache = false));
return nothing;
}
- return cache(
- this.view === GerritView.DIFF ? this.diffViewTemplate() : nothing
- );
+ return cache(this.isDiffView() ? this.diffViewTemplate() : nothing);
}
private diffViewTemplate() {
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 3008236..0261992 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -13,8 +13,6 @@
import {SearchViewState} from '../models/views/search';
import {DashboardViewState} from '../models/views/dashboard';
import {ChangeViewState} from '../models/views/change';
-import {DiffViewState} from '../models/views/diff';
-import {EditViewState} from '../models/views/edit';
export interface AppElement extends HTMLElement {
params: AppElementParams;
@@ -41,8 +39,6 @@
| SearchViewState
| SettingsViewState
| AgreementViewState
- | DiffViewState
- | EditViewState
| AppElementJustRegisteredParams;
export function isAppElementJustRegisteredParams(
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 1549da5..0e75abb 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -95,12 +95,26 @@
.lengthCounter {
font-weight: var(--font-weight-normal);
}
+ p {
+ max-width: 65ch;
+ margin-bottom: var(--spacing-m);
+ }
`,
];
override render() {
if (!this.account || this.loading) return nothing;
return html`<div class="gr-form-styles">
+ <p>
+ All profile fields below may be publicly displayed to others, including
+ on changes you are associated with, as well as in search and
+ autocompletion.
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-privacy.html"
+ >Learn more</a
+ >
+ </p>
+ <gr-endpoint-decorator name="profile"></gr-endpoint-decorator>
<section>
<span class="title"></span>
<span class="value">
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
index e968b12..f954960 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -69,6 +69,16 @@
element,
/* HTML */ `
<div class="gr-form-styles">
+ <p>
+ All profile fields below may be publicly displayed to others,
+ including on changes you are associated with, as well as in search
+ and autocompletion.
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-privacy.html"
+ >Learn more</a
+ >
+ </p>
+ <gr-endpoint-decorator name="profile"></gr-endpoint-decorator>
<section>
<span class="title"></span>
<span class="value">
@@ -134,7 +144,8 @@
</span>
</section>
</div>
- `
+ `,
+ {ignoreChildren: ['p']}
);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index df41b1f4..dd0fbca 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -72,8 +72,7 @@
import {whenRendered} from '../../../utils/dom-util';
import {Interaction} from '../../../constants/reporting';
import {HtmlPatched} from '../../../utils/lit-util';
-import {createDiffUrl} from '../../../models/views/diff';
-import {createChangeUrl} from '../../../models/views/change';
+import {createChangeUrl, createDiffUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {highlightServiceToken} from '../../../services/highlight/highlight-service';
@@ -746,8 +745,8 @@
return createDiffUrl({
changeNum: this.changeNum,
repo: this.repoName,
- path: this.thread.path,
patchNum: this.thread.patchNum,
+ diffView: {path: this.thread.path},
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 090dfef..66beaf1 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -60,7 +60,7 @@
import {Interaction} from '../../../constants/reporting';
import {KnownExperimentId} from '../../../services/flags/flags';
import {isBase64FileContent} from '../../../api/rest-api';
-import {createDiffUrl} from '../../../models/views/diff';
+import {createDiffUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -547,8 +547,9 @@
${this.renderDraftLabel()}
</div>
<div class="headerMiddle">${this.renderCollapsedContent()}</div>
- ${this.renderRunDetails()} ${this.renderDeleteButton()}
- ${this.renderPatchset()} ${this.renderDate()} ${this.renderToggle()}
+ ${this.renderSuggestEditButton()} ${this.renderRunDetails()}
+ ${this.renderDeleteButton()} ${this.renderPatchset()}
+ ${this.renderDate()} ${this.renderToggle()}
</div>
`;
}
@@ -777,10 +778,9 @@
return html`
<div class="rightActions">
${this.autoSaving ? html`. ` : ''}
- ${this.renderDiscardButton()} ${this.renderSuggestEditButton()}
- ${this.renderPreviewSuggestEditButton()} ${this.renderEditButton()}
- ${this.renderCancelButton()} ${this.renderSaveButton()}
- ${this.renderCopyLinkIcon()}
+ ${this.renderDiscardButton()} ${this.renderPreviewSuggestEditButton()}
+ ${this.renderEditButton()} ${this.renderCancelButton()}
+ ${this.renderSaveButton()} ${this.renderCopyLinkIcon()}
</div>
`;
}
@@ -809,6 +809,7 @@
return nothing;
}
if (
+ !this.editing ||
this.permanentEditingMode ||
this.comment?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
) {
@@ -1139,7 +1140,8 @@
fire(this, 'open-fix-preview', await this.createFixPreview());
}
- async createSuggestEdit() {
+ async createSuggestEdit(e: MouseEvent) {
+ e.stopPropagation();
const line = await this.getCommentedCode();
this.messageText += `${USER_SUGGESTION_START_PATTERN}${line}${'\n```'}`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index ec9c875..3390369 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -854,12 +854,12 @@
.initiallyCollapsed=${false}
></gr-comment>`
);
+ element.editing = true;
});
test('renders suggest fix button', () => {
assert.dom.equal(
queryAndAssert(element, 'gr-button.suggestEdit'),
/* HTML */ `<gr-button
- aria-disabled="false"
class="action suggestEdit"
link=""
role="button"
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 1460246..f8ae38c 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -553,7 +553,7 @@
assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
});
- test('emoji dropdown is closed when iron-overlay-closed is fired', async () => {
+ test('emoji dropdown is closed when dropdown-closed is fired', async () => {
const resetSpy = sinon.spy(element, 'closeDropdown');
element.emojiSuggestions!.dispatchEvent(
new CustomEvent('dropdown-closed', {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index 3392951..87fd5ca 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -48,6 +48,12 @@
}, 0);
}
+export function isFileUnchanged(diff: DiffInfo) {
+ return !diff.content.some(
+ content => (content.a && !content.common) || (content.b && !content.common)
+ );
+}
+
export function getResponsiveMode(
prefs?: DiffPreferencesInfo,
renderPrefs?: RenderPreferences
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 98b4586..25dc768 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '@open-wc/testing';
+import {DiffInfo} from '../../../api/diff';
import '../../../test/common-test-setup';
-import {createElementDiff, formatText, createTabWrapper} from './gr-diff-utils';
+import {createDiff} from '../../../test/test-data-generators';
+import {
+ createElementDiff,
+ formatText,
+ createTabWrapper,
+ isFileUnchanged,
+} from './gr-diff-utils';
const LINE_BREAK_HTML = '<span class="gr-diff br"></span>';
@@ -156,4 +163,36 @@
expectTextLength('abc\tde\t', 10, 20);
expectTextLength('\t\t\t\t\t', 20, 100);
});
+
+ test('isFileUnchanged', () => {
+ let diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {a: ['abcd'], ab: ['ef']},
+ {b: ['ancd'], a: ['xx']},
+ ],
+ };
+ assert.equal(isFileUnchanged(diff), false);
+ diff = {
+ ...createDiff(),
+ content: [{ab: ['abcd']}, {ab: ['ancd']}],
+ };
+ assert.equal(isFileUnchanged(diff), true);
+ diff = {
+ ...createDiff(),
+ content: [
+ {a: ['abcd'], ab: ['ef'], common: true},
+ {b: ['ancd'], ab: ['xx']},
+ ],
+ };
+ assert.equal(isFileUnchanged(diff), false);
+ diff = {
+ ...createDiff(),
+ content: [
+ {a: ['abcd'], ab: ['ef'], common: true},
+ {b: ['ancd'], ab: ['xx'], common: true},
+ ],
+ };
+ assert.equal(isFileUnchanged(diff), true);
+ });
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index 2097170..a0526ed 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -755,9 +755,9 @@
rule wins in case of same specificity.
*/
.trailing-whitespace,
- .content .trailing-whitespace,
+ .content .contentText .trailing-whitespace,
.trailing-whitespace .intraline,
- .content .trailing-whitespace .intraline {
+ .content .contentText .trailing-whitespace .intraline {
border-radius: var(--border-radius, 4px);
background-color: var(--diff-trailing-whitespace-indicator);
}
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 8282a3f..446822f 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -12,6 +12,7 @@
PatchSetNum,
PreferencesInfo,
RevisionPatchSetNum,
+ PatchSetNumber,
} from '../../types/common';
import {DefaultBase} from '../../constants/constants';
import {combineLatest, from, fromEvent, Observable, forkJoin, of} from 'rxjs';
@@ -22,7 +23,6 @@
startWith,
switchMap,
} from 'rxjs/operators';
-import {RouterModel} from '../../services/router/router-model';
import {
computeAllPatchSets,
computeLatestPatchNum,
@@ -38,6 +38,13 @@
import {UserModel} from '../user/user-model';
import {define} from '../dependency';
import {isOwner} from '../../utils/change-util';
+import {
+ ChangeViewModel,
+ createChangeUrl,
+ createDiffUrl,
+ createEditUrl,
+} from '../views/change';
+import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
export enum LoadingStatus {
NOT_LOADED = 'NOT_LOADED',
@@ -56,12 +63,6 @@
loadingStatus: LoadingStatus;
change?: ParsedChangeInfo;
/**
- * The name of the file user is viewing in the diff view mode. File path is
- * specified in the url or derived from the commentId.
- * Does not apply to change-view or edit-view.
- */
- diffPath?: string;
- /**
* The list of reviewed files, kept in the model because we want changes made
* in one view to reflect on other views without re-rendering the other views.
* Undefined means it's still loading and empty set means no files reviewed.
@@ -76,7 +77,7 @@
export function updateChangeWithEdit(
change?: ParsedChangeInfo,
edit?: EditInfo,
- routerPatchNum?: PatchSetNum
+ viewModelPatchNum?: PatchSetNum
): ParsedChangeInfo | undefined {
if (!change || !edit) return change;
assertIsDefined(edit.commit.commit, 'edit.commit.commit');
@@ -95,7 +96,7 @@
// which is still done in change-view. `_patchRange.patchNum` should
// eventually also be model managed, so we can reconcile these two code
// snippets into one location.
- if (routerPatchNum === undefined) {
+ if (viewModelPatchNum === undefined) {
change.current_revision = edit.commit.commit;
}
return change;
@@ -103,20 +104,20 @@
/**
* Derives the base patchset number from all the data that can potentially
- * influence it. Mostly just returns `routerBasePatchNum` or PARENT, but has
+ * influence it. Mostly just returns `viewModelBasePatchNum` or PARENT, but has
* some special logic when looking at merge commits.
*
- * NOTE: At the moment this returns just `routerBasePatchNum ?? PARENT`, see
+ * NOTE: At the moment this returns just `viewModelBasePatchNum ?? PARENT`, see
* TODO below.
*/
function computeBase(
- routerBasePatchNum: BasePatchSetNum | undefined,
+ viewModelBasePatchNum: BasePatchSetNum | undefined,
patchNum: RevisionPatchSetNum | undefined,
change: ParsedChangeInfo | undefined,
preferences: PreferencesInfo
): BasePatchSetNum {
- if (routerBasePatchNum && routerBasePatchNum !== PARENT) {
- return routerBasePatchNum;
+ if (viewModelBasePatchNum && viewModelBasePatchNum !== PARENT) {
+ return viewModelBasePatchNum;
}
if (!change || !patchNum) return PARENT;
@@ -129,7 +130,7 @@
// but we are not sure whether this was ever 100% working correctly. A
// major challenge is being able to select PARENT explicitly even if your
// preference for the default choice is FIRST_PARENT. <gr-file-list-header>
- // just uses `navigation.setUrl()` and the router does not have any
+ // just uses `navigation.setUrl()` and the view model does not have any
// way of forcing the basePatchSetNum to stick to PARENT without being
// altered back to FIRST_PARENT here.
// See also corresponding TODO in gr-settings-view.
@@ -150,7 +151,11 @@
export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
- private patchNum?: PatchSetNum;
+ private patchNum?: RevisionPatchSetNum;
+
+ private basePatchNum?: BasePatchSetNum;
+
+ private latestPatchNum?: PatchSetNumber;
public readonly change$ = select(
this.state$,
@@ -162,11 +167,6 @@
changeState => changeState.loadingStatus
);
- public readonly diffPath$ = select(
- this.state$,
- changeState => changeState?.diffPath
- );
-
public readonly reviewedFiles$ = select(
this.state$,
changeState => changeState?.reviewedFiles
@@ -178,8 +178,17 @@
public readonly labels$ = select(this.change$, change => change?.labels);
- public readonly latestPatchNum$ = select(this.change$, change =>
- computeLatestPatchNum(computeAllPatchSets(change))
+ public readonly revisions$ = select(
+ this.change$,
+ change => change?.revisions
+ );
+
+ public readonly patchsets$ = select(this.change$, change =>
+ computeAllPatchSets(change)
+ );
+
+ public readonly latestPatchNum$ = select(this.patchsets$, patchsets =>
+ computeLatestPatchNum(patchsets)
);
/**
@@ -192,57 +201,57 @@
public readonly patchNum$: Observable<RevisionPatchSetNum | undefined> =
select(
combineLatest([
- this.routerModel.state$,
+ this.viewModel.state$,
this.state$,
this.latestPatchNum$,
]).pipe(
/**
- * If you depend on both, router and change state, then you want to
- * filter out inconsistent state, e.g. router changeNum already updated,
- * change not yet reset to undefined.
+ * If you depend on both, view model and change state, then you want to
+ * filter out inconsistent state, e.g. view model changeNum already
+ * updated, change not yet reset to undefined.
*/
- filter(([routerState, changeState, _latestPatchN]) => {
+ filter(([viewModelState, changeState, _latestPatchN]) => {
const changeNum = changeState.change?._number;
- const routerChangeNum = routerState.changeNum;
- return changeNum === undefined || changeNum === routerChangeNum;
+ const viewModelChangeNum = viewModelState?.changeNum;
+ return changeNum === undefined || changeNum === viewModelChangeNum;
})
),
- ([routerState, _changeState, latestPatchN]) =>
- routerState?.patchNum || latestPatchN
+ ([viewModelState, _changeState, latestPatchN]) =>
+ viewModelState?.patchNum || latestPatchN
);
/**
* Emits the base patchset number. This is identical to the
- * `routerBasePatchNum$`, but has some special logic for merges.
+ * `viewModel.basePatchNum$`, but has some special logic for merges.
*
* Note that this selector can emit without the change being available!
*/
public readonly basePatchNum$: Observable<BasePatchSetNum> =
/**
- * If you depend on both, router and change state, then you want to filter
- * out inconsistent state, e.g. router changeNum already updated, change not
- * yet reset to undefined.
+ * If you depend on both, view model and change state, then you want to
+ * filter out inconsistent state, e.g. view model changeNum already
+ * updated, change not yet reset to undefined.
*/
select(
combineLatest([
- this.routerModel.state$,
+ this.viewModel.state$,
this.state$,
this.userModel.state$,
]).pipe(
- filter(([routerState, changeState, _]) => {
+ filter(([viewModelState, changeState, _]) => {
const changeNum = changeState.change?._number;
- const routerChangeNum = routerState.changeNum;
- return changeNum === undefined || changeNum === routerChangeNum;
+ const viewModelChangeNum = viewModelState?.changeNum;
+ return changeNum === undefined || changeNum === viewModelChangeNum;
}),
withLatestFrom(
- this.routerModel.routerBasePatchNum$,
+ this.viewModel.basePatchNum$,
this.patchNum$,
this.change$,
this.userModel.preferences$
)
),
- ([_, routerBasePatchNum, patchNum, change, preferences]) =>
- computeBase(routerBasePatchNum, patchNum, change, preferences)
+ ([_, viewModelBasePatchNum, patchNum, change, preferences]) =>
+ computeBase(viewModelBasePatchNum, patchNum, change, preferences)
);
public readonly isOwner$: Observable<boolean> = select(
@@ -257,13 +266,14 @@
);
constructor(
- private readonly routerModel: RouterModel,
+ private readonly navigation: NavigationService,
+ private readonly viewModel: ChangeViewModel,
private readonly restApiService: RestApiService,
private readonly userModel: UserModel
) {
super(initialState);
this.subscriptions = [
- combineLatest([this.routerModel.routerChangeNum$, this.reload$])
+ combineLatest([this.viewModel.changeNum$, this.reload$])
.pipe(
map(([changeNum, _]) => changeNum),
switchMap(changeNum => {
@@ -272,7 +282,7 @@
const edit = from(this.restApiService.getChangeEdit(changeNum));
return forkJoin([change, edit]);
}),
- withLatestFrom(this.routerModel.routerPatchNum$),
+ withLatestFrom(this.viewModel.patchNum$),
map(([[change, edit], patchNum]) =>
updateChangeWithEdit(change, edit, patchNum)
)
@@ -289,6 +299,12 @@
}),
this.change$.subscribe(change => (this.change = change)),
this.patchNum$.subscribe(patchNum => (this.patchNum = patchNum)),
+ this.basePatchNum$.subscribe(
+ basePatchNum => (this.basePatchNum = basePatchNum)
+ ),
+ this.latestPatchNum$.subscribe(
+ latestPatchNum => (this.latestPatchNum = latestPatchNum)
+ ),
combineLatest([this.patchNum$, this.changeNum$, this.userModel.loggedIn$])
.pipe(
switchMap(([patchNum, changeNum, loggedIn]) => {
@@ -303,11 +319,6 @@
];
}
- // Temporary workaround until path is derived in the model itself.
- updatePath(diffPath?: string) {
- this.updateState({diffPath});
- }
-
updateStateReviewedFiles(reviewedFiles: string[]) {
this.updateState({reviewedFiles});
}
@@ -372,6 +383,65 @@
return this.getState().change;
}
+ diffUrl(
+ diffView: {path: string; lineNum?: number},
+ patchNum = this.patchNum,
+ basePatchNum = this.basePatchNum
+ ) {
+ if (!this.change) return;
+ if (!this.patchNum) return;
+ return createDiffUrl({
+ change: this.change,
+ patchNum,
+ basePatchNum,
+ diffView,
+ });
+ }
+
+ navigateToDiff(
+ diffView: {path: string; lineNum?: number},
+ patchNum = this.patchNum,
+ basePatchNum = this.basePatchNum
+ ) {
+ const url = this.diffUrl(diffView, patchNum, basePatchNum);
+ if (!url) return;
+ this.navigation.setUrl(url);
+ }
+
+ changeUrl(openReplyDialog = false) {
+ if (!this.change) return;
+ const isLatest = this.latestPatchNum === this.patchNum;
+ return createChangeUrl({
+ change: this.change,
+ patchNum:
+ isLatest && this.basePatchNum === PARENT ? undefined : this.patchNum,
+ basePatchNum: this.basePatchNum,
+ openReplyDialog,
+ });
+ }
+
+ navigateToChange(openReplyDialog = false) {
+ const url = this.changeUrl(openReplyDialog);
+ if (!url) return;
+ this.navigation.setUrl(url);
+ }
+
+ editUrl(editView: {path: string; lineNum?: number}) {
+ if (!this.change) return;
+ return createEditUrl({
+ changeNum: this.change._number,
+ repo: this.change.project,
+ patchNum: this.patchNum,
+ editView,
+ });
+ }
+
+ navigateToEdit(editView: {path: string; lineNum?: number}) {
+ const url = this.editUrl(editView);
+ if (!url) return;
+ this.navigation.setUrl(url);
+ }
+
/**
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index a2fc7c9..c11c15b 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -9,6 +9,7 @@
import {
createChange,
createChangeMessageInfo,
+ createChangeViewState,
createEditInfo,
createParsedChange,
createRevision,
@@ -28,12 +29,13 @@
} from '../../types/common';
import {ParsedChangeInfo} from '../../types/types';
import {getAppContext} from '../../services/app-context';
-import {GerritView, routerModelToken} from '../../services/router/router-model';
import {ChangeState, LoadingStatus, updateChangeWithEdit} from './change-model';
import {ChangeModel} from './change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {userModelToken} from '../user/user-model';
+import {changeViewModelToken} from '../views/change';
+import {navigationToken} from '../../elements/core/gr-navigation/gr-navigation';
suite('updateChangeWithEdit() tests', () => {
test('undefined change', async () => {
@@ -83,7 +85,8 @@
setup(() => {
changeModel = new ChangeModel(
- testResolver(routerModelToken),
+ testResolver(navigationToken),
+ testResolver(changeViewModelToken),
getAppContext().restApiService,
testResolver(userModelToken)
);
@@ -121,10 +124,7 @@
assert.equal(stub.callCount, 0);
assert.isUndefined(state?.change);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
state = await waitForLoadingStatus(LoadingStatus.LOADING);
assert.equal(stub.callCount, 1);
assert.isUndefined(state?.change);
@@ -140,10 +140,7 @@
const promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
@@ -164,10 +161,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
@@ -178,8 +172,8 @@
_number: 123 as NumericChangeId,
};
promise = mockPromise<ParsedChangeInfo | undefined>();
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
+ testResolver(changeViewModelToken).setState({
+ ...createChangeViewState(),
changeNum: otherChange._number,
});
state = await waitForLoadingStatus(LoadingStatus.LOADING);
@@ -197,10 +191,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
@@ -208,10 +199,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(undefined);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: undefined,
- });
+ testResolver(changeViewModelToken).setState(undefined);
state = await waitForLoadingStatus(LoadingStatus.NOT_LOADED);
assert.equal(stub.callCount, 2);
assert.isUndefined(state?.change);
@@ -220,10 +208,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(knownChange);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
state = await waitForLoadingStatus(LoadingStatus.LOADED);
assert.equal(stub.callCount, 3);
assert.equal(state?.change, knownChange);
@@ -299,7 +284,7 @@
assert.equal(spy.lastCall.firstArg, PARENT);
// test update
- testResolver(routerModelToken).updateState({
+ testResolver(changeViewModelToken).updateState({
basePatchNum: 1 as PatchSetNumber,
});
assert.equal(spy.callCount, 2);
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 07e64a2..0683af0 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -23,6 +23,9 @@
import {ChangeModel} from './change-model';
import {CommentsModel} from '../comments/comments-model';
+export type FileNameToNormalizedFileInfoMap = {
+ [name: string]: NormalizedFileInfo;
+};
export interface NormalizedFileInfo extends FileInfo {
__path: string;
// Compared to `FileInfo` these four props are required here.
@@ -115,7 +118,12 @@
export class FilesModel extends Model<FilesState> {
public readonly files$ = select(this.state$, state => state.files);
- public readonly filesWithUnmodified$ = select(
+ /**
+ * `files$` only includes the files that were modified. Here we also include
+ * all unmodified files that have comments with
+ * `status: FileInfoStatus.UNMODIFIED`.
+ */
+ public readonly filesIncludingUnmodified$ = select(
combineLatest([this.files$, this.commentsModel.commentedPaths$]),
([files, commentedPaths]) => addUnmodified(files, commentedPaths)
);
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index 51b4591..1fdf342 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -29,7 +29,6 @@
} from '../../utils/comment-util';
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
-import {RouterModel} from '../../services/router/router-model';
import {define} from '../dependency';
import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
import {fire, fireAlert, fireEvent} from '../../utils/event-util';
@@ -54,6 +53,7 @@
switchMap,
} from 'rxjs/operators';
import {isDefined} from '../../types/types';
+import {ChangeViewModel} from '../views/change';
export interface CommentState {
/** undefined means 'still loading' */
@@ -415,7 +415,7 @@
private discardedDrafts: DraftInfo[] = [];
constructor(
- private readonly routerModel: RouterModel,
+ private readonly changeViewModel: ChangeViewModel,
private readonly changeModel: ChangeModel,
private readonly accountsModel: AccountsModel,
private readonly restApiService: RestApiService,
@@ -432,7 +432,7 @@
this.changeModel.patchNum$.subscribe(x => (this.patchNum = x))
);
this.subscriptions.push(
- this.routerModel.routerChangeNum$.subscribe(changeNum => {
+ this.changeViewModel.changeNum$.subscribe(changeNum => {
this.changeNum = changeNum;
this.setState({...initialState});
this.reloadAllComments();
diff --git a/polygerrit-ui/app/models/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
index cab36d2..a689e42 100644
--- a/polygerrit-ui/app/models/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -6,6 +6,7 @@
import '../../test/common-test-setup';
import {
createAccountWithEmail,
+ createChangeViewState,
createDraft,
} from '../../test/test-data-generators';
import {
@@ -20,17 +21,16 @@
import {
createComment,
createParsedChange,
- TEST_NUMERIC_CHANGE_ID,
} from '../../test/test-data-generators';
import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
import {getAppContext} from '../../services/app-context';
-import {GerritView, routerModelToken} from '../../services/router/router-model';
import {PathToCommentsInfoMap} from '../../types/common';
import {changeModelToken} from '../change/change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {accountsModelToken} from '../accounts-model/accounts-model';
import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api';
+import {changeViewModelToken} from '../views/change';
suite('comments model tests', () => {
test('updateStateDeleteDraft', () => {
@@ -72,7 +72,7 @@
test('loads comments', async () => {
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
@@ -100,10 +100,7 @@
model.portedComments$.subscribe(c => (portedComments = c ?? {}))
);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: TEST_NUMERIC_CHANGE_ID,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
testResolver(changeModelToken).updateStateChange(createParsedChange());
await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
@@ -133,7 +130,7 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
@@ -161,7 +158,7 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
@@ -199,7 +196,7 @@
})
);
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 31d511a..a206037 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -10,6 +10,7 @@
BasePatchSetNum,
ChangeInfo,
PatchSetNumber,
+ EDIT,
} from '../../api/rest-api';
import {Tab} from '../../constants/constants';
import {GerritView} from '../../services/router/router-model';
@@ -26,18 +27,31 @@
import {Model} from '../model';
import {ViewState} from './base';
+export enum ChangeChildView {
+ OVERVIEW = 'OVERVIEW',
+ DIFF = 'DIFF',
+ EDIT = 'EDIT',
+}
+
export interface ChangeViewState extends ViewState {
view: GerritView.CHANGE;
+ childView: ChangeChildView;
changeNum: NumericChangeId;
repo: RepoName;
- edit?: boolean;
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
+ /** Refers to comment on COMMENTS tab in OVERVIEW. */
commentId?: UrlEncodedCommentId;
+
+ // TODO: Move properties that only apply to OVERVIEW into a submessage.
+
+ edit?: boolean;
/** This can be a string only for plugin provided tabs. */
tab?: Tab | string;
+ // TODO: Move properties that only apply to CHECKS tab into a submessage.
+
/** Checks related view state */
/** selected patchset for check runs (undefined=latest) */
@@ -61,6 +75,19 @@
forceReload?: boolean;
/** triggers opening the reply dialog */
openReplyDialog?: boolean;
+
+ /** These properties apply to the DIFF child view only. */
+ diffView?: {
+ path?: string;
+ lineNum?: number;
+ leftSide?: boolean;
+ };
+
+ /** These properties apply to the EDIT child view only. */
+ editView?: {
+ path?: string;
+ lineNum?: number;
+ };
}
/**
@@ -70,7 +97,7 @@
*/
export type CreateChangeUrlObject = Omit<
ChangeViewState,
- 'view' | 'changeNum' | 'repo'
+ 'view' | 'childView' | 'changeNum' | 'repo'
> & {
change: Pick<ChangeInfo, '_number' | 'project'>;
};
@@ -82,7 +109,9 @@
}
export function objToState(
- obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view'>
+ obj:
+ | (CreateChangeUrlObject & {childView: ChangeChildView})
+ | Omit<ChangeViewState, 'view'>
): ChangeViewState {
if (isCreateChangeUrlObject(obj)) {
return {
@@ -95,15 +124,26 @@
return {...obj, view: GerritView.CHANGE};
}
-export function createChangeUrl(
- obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view'>
-) {
- const state: ChangeViewState = objToState(obj);
- let range = getPatchRangeExpression(state);
- if (range.length) {
- range = '/' + range;
+export function createChangeViewUrl(state: ChangeViewState): string {
+ switch (state.childView) {
+ case ChangeChildView.OVERVIEW:
+ return createChangeUrl(state);
+ case ChangeChildView.DIFF:
+ return createDiffUrl(state);
+ case ChangeChildView.EDIT:
+ return createEditUrl(state);
}
- let suffix = `${range}`;
+}
+
+export function createChangeUrl(
+ obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>
+) {
+ const state: ChangeViewState = objToState({
+ ...obj,
+ childView: ChangeChildView.OVERVIEW,
+ });
+
+ let suffix = '';
const queries = [];
if (state.checksPatchset && state.checksPatchset > 0) {
queries.push(`checksPatchset=${state.checksPatchset}`);
@@ -136,7 +176,7 @@
suffix += ',edit';
}
if (state.commentId) {
- suffix = suffix + `/comments/${state.commentId}`;
+ suffix += `/comments/${state.commentId}`;
}
if (queries.length > 0) {
suffix += '?' + queries.join('&');
@@ -144,18 +184,99 @@
if (state.messageHash) {
suffix += state.messageHash;
}
- if (state.repo) {
- const encodedProject = encodeURL(state.repo, true);
- return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
- } else {
- return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
+
+ return `${createChangeUrlCommon(state)}${suffix}`;
+}
+
+export function createDiffUrl(
+ obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>
+) {
+ const state: ChangeViewState = objToState({
+ ...obj,
+ childView: ChangeChildView.DIFF,
+ });
+
+ const path = `/${encodeURL(state.diffView?.path ?? '', true)}`;
+
+ let suffix = '';
+ // TODO: Move creating of comment URLs to a separate function. We are
+ // "abusing" the `commentId` property, which should only be used for pointing
+ // to comment in the COMMENTS tab of the OVERVIEW page.
+ if (state.commentId) {
+ suffix += `comment/${state.commentId}/`;
}
+
+ if (state.diffView?.lineNum) {
+ suffix += '#';
+ if (state.diffView?.leftSide) {
+ suffix += 'b';
+ }
+ suffix += state.diffView.lineNum;
+ }
+
+ return `${createChangeUrlCommon(state)}${path}${suffix}`;
+}
+
+export function createEditUrl(
+ obj: Omit<ChangeViewState, 'view' | 'childView'>
+): string {
+ const state: ChangeViewState = objToState({
+ ...obj,
+ childView: ChangeChildView.DIFF,
+ patchNum: obj.patchNum ?? EDIT,
+ });
+
+ const path = `/${encodeURL(state.editView?.path ?? '', true)}`;
+ const line = state.editView?.lineNum;
+ const suffix = line ? `#${line}` : '';
+
+ return `${createChangeUrlCommon(state)}${path},edit${suffix}`;
+}
+
+/**
+ * The shared part of creating a change URL between OVERVIEW, DIFF and EDIT
+ * child views.
+ */
+function createChangeUrlCommon(state: ChangeViewState) {
+ let range = getPatchRangeExpression(state);
+ if (range.length) range = '/' + range;
+
+ let repo = '';
+ if (state.repo) repo = `${encodeURL(state.repo, true)}/+/`;
+
+ return `${getBaseUrl()}/c/${repo}${state.changeNum}${range}`;
}
export const changeViewModelToken =
define<ChangeViewModel>('change-view-model');
export class ChangeViewModel extends Model<ChangeViewState | undefined> {
+ public readonly changeNum$ = select(this.state$, state => state?.changeNum);
+
+ public readonly patchNum$ = select(this.state$, state => state?.patchNum);
+
+ public readonly basePatchNum$ = select(
+ this.state$,
+ state => state?.basePatchNum
+ );
+
+ public readonly diffPath$ = select(
+ this.state$,
+ state => state?.diffView?.path
+ );
+
+ public readonly diffLine$ = select(
+ this.state$,
+ state => state?.diffView?.lineNum
+ );
+
+ public readonly diffLeftSide$ = select(
+ this.state$,
+ state => state?.diffView?.leftSide ?? false
+ );
+
+ public readonly childView$ = select(this.state$, state => state?.childView);
+
public readonly tab$ = select(this.state$, state => state?.tab);
public readonly checksPatchset$ = select(
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index b34a1ba..837e362 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -6,73 +6,145 @@
import {assert} from '@open-wc/testing';
import {
BasePatchSetNum,
- NumericChangeId,
RepoName,
RevisionPatchSetNum,
} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
import '../../test/common-test-setup';
-import {createChangeUrl, ChangeViewState} from './change';
-
-const STATE: ChangeViewState = {
- view: GerritView.CHANGE,
- changeNum: 1234 as NumericChangeId,
- repo: 'test' as RepoName,
-};
+import {
+ createChangeViewState,
+ createDiffViewState,
+ createEditViewState,
+} from '../../test/test-data-generators';
+import {
+ createChangeUrl,
+ createDiffUrl,
+ createEditUrl,
+ ChangeViewState,
+} from './change';
suite('change view state tests', () => {
test('createChangeUrl()', () => {
- const state: ChangeViewState = {...STATE};
+ const state: ChangeViewState = createChangeViewState();
- assert.equal(createChangeUrl(state), '/c/test/+/1234');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42');
state.patchNum = 10 as RevisionPatchSetNum;
- assert.equal(createChangeUrl(state), '/c/test/+/1234/10');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42/10');
state.basePatchNum = 5 as BasePatchSetNum;
- assert.equal(createChangeUrl(state), '/c/test/+/1234/5..10');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42/5..10');
state.messageHash = '#123';
- assert.equal(createChangeUrl(state), '/c/test/+/1234/5..10#123');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42/5..10#123');
});
test('createChangeUrl() baseUrl', () => {
window.CANONICAL_PATH = '/base';
- const state: ChangeViewState = {...STATE};
+ const state: ChangeViewState = createChangeViewState();
assert.equal(createChangeUrl(state).substring(0, 5), '/base');
window.CANONICAL_PATH = undefined;
});
test('createChangeUrl() checksRunsSelected', () => {
const state: ChangeViewState = {
- ...STATE,
+ ...createChangeViewState(),
checksRunsSelected: new Set(['asdf']),
};
assert.equal(
createChangeUrl(state),
- '/c/test/+/1234?checksRunsSelected=asdf'
+ '/c/test-project/+/42?checksRunsSelected=asdf'
);
});
test('createChangeUrl() checksResultsFilter', () => {
const state: ChangeViewState = {
- ...STATE,
+ ...createChangeViewState(),
checksResultsFilter: 'asdf.*qwer',
};
assert.equal(
createChangeUrl(state),
- '/c/test/+/1234?checksResultsFilter=asdf.*qwer'
+ '/c/test-project/+/42?checksResultsFilter=asdf.*qwer'
);
});
test('createChangeUrl() with repo name encoding', () => {
const state: ChangeViewState = {
- view: GerritView.CHANGE,
- changeNum: 1234 as NumericChangeId,
+ ...createChangeViewState(),
repo: 'x+/y+/z+/w' as RepoName,
};
- assert.equal(createChangeUrl(state), '/c/x%252B/y%252B/z%252B/w/+/1234');
+ assert.equal(createChangeUrl(state), '/c/x%252B/y%252B/z%252B/w/+/42');
+ });
+
+ test('createDiffUrl', () => {
+ const params: ChangeViewState = {
+ ...createDiffViewState(),
+ patchNum: 12 as RevisionPatchSetNum,
+ diffView: {path: 'x+y/path.cpp'},
+ };
+ assert.equal(
+ createDiffUrl(params),
+ '/c/test-project/+/42/12/x%252By/path.cpp'
+ );
+
+ window.CANONICAL_PATH = '/base';
+ assert.equal(createDiffUrl(params).substring(0, 5), '/base');
+ window.CANONICAL_PATH = undefined;
+
+ params.repo = 'test' as RepoName;
+ assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
+
+ params.basePatchNum = 6 as BasePatchSetNum;
+ assert.equal(createDiffUrl(params), '/c/test/+/42/6..12/x%252By/path.cpp');
+
+ params.diffView = {
+ path: 'foo bar/my+file.txt%',
+ };
+ params.patchNum = 2 as RevisionPatchSetNum;
+ delete params.basePatchNum;
+ assert.equal(
+ createDiffUrl(params),
+ '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'
+ );
+
+ params.diffView = {
+ path: 'file.cpp',
+ lineNum: 123,
+ };
+ assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#123');
+
+ params.diffView = {
+ path: 'file.cpp',
+ lineNum: 123,
+ leftSide: true,
+ };
+ assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#b123');
+ });
+
+ test('diff with repo name encoding', () => {
+ const params: ChangeViewState = {
+ ...createDiffViewState(),
+ patchNum: 12 as RevisionPatchSetNum,
+ repo: 'x+/y' as RepoName,
+ diffView: {path: 'x+y/path.cpp'},
+ };
+ assert.equal(createDiffUrl(params), '/c/x%252B/y/+/42/12/x%252By/path.cpp');
+ });
+
+ test('createEditUrl', () => {
+ const params: ChangeViewState = {
+ ...createEditViewState(),
+ patchNum: 12 as RevisionPatchSetNum,
+ editView: {path: 'x+y/path.cpp' as RepoName, lineNum: 31},
+ };
+ assert.equal(
+ createEditUrl(params),
+ '/c/test-project/+/42/12/x%252By/path.cpp,edit#31'
+ );
+
+ window.CANONICAL_PATH = '/base';
+ assert.equal(createEditUrl(params).substring(0, 5), '/base');
+ window.CANONICAL_PATH = undefined;
});
});
diff --git a/polygerrit-ui/app/models/views/diff.ts b/polygerrit-ui/app/models/views/diff.ts
deleted file mode 100644
index 34f4ee7..0000000
--- a/polygerrit-ui/app/models/views/diff.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
- BasePatchSetNum,
- ChangeInfo,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import {UrlEncodedCommentId} from '../../types/common';
-import {
- encodeURL,
- getBaseUrl,
- getPatchRangeExpression,
-} from '../../utils/url-util';
-import {define} from '../dependency';
-import {Model} from '../model';
-import {ViewState} from './base';
-
-export interface DiffViewState extends ViewState {
- view: GerritView.DIFF;
- changeNum: NumericChangeId;
- repo?: RepoName;
- commentId?: UrlEncodedCommentId;
- path?: string;
- patchNum?: RevisionPatchSetNum;
- basePatchNum?: BasePatchSetNum;
- lineNum?: number;
- leftSide?: boolean;
- commentLink?: boolean;
-}
-
-/**
- * This is a convenience type such that you can pass a `ChangeInfo` object
- * as the `change` property instead of having to set both the `changeNum` and
- * `project` properties explicitly.
- */
-export type CreateChangeUrlObject = Omit<
- DiffViewState,
- 'view' | 'changeNum' | 'project'
-> & {
- change: Pick<ChangeInfo, '_number' | 'project'>;
-};
-
-export function isCreateChangeUrlObject(
- state: CreateChangeUrlObject | Omit<DiffViewState, 'view'>
-): state is CreateChangeUrlObject {
- return !!(state as CreateChangeUrlObject).change;
-}
-
-export function objToState(
- obj: CreateChangeUrlObject | Omit<DiffViewState, 'view'>
-): DiffViewState {
- if (isCreateChangeUrlObject(obj)) {
- return {
- ...obj,
- view: GerritView.DIFF,
- changeNum: obj.change._number,
- repo: obj.change.project,
- };
- }
- return {...obj, view: GerritView.DIFF};
-}
-
-export function createDiffUrl(
- obj: CreateChangeUrlObject | Omit<DiffViewState, 'view'>
-) {
- const state: DiffViewState = objToState(obj);
- let range = getPatchRangeExpression(state);
- if (range.length) range = '/' + range;
-
- let suffix = `${range}/${encodeURL(state.path || '', true)}`;
-
- if (state.lineNum) {
- suffix += '#';
- if (state.leftSide) {
- suffix += 'b';
- }
- suffix += state.lineNum;
- }
-
- if (state.commentId) {
- suffix = `/comment/${state.commentId}` + suffix;
- }
-
- if (state.repo) {
- const encodedProject = encodeURL(state.repo, true);
- return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
- } else {
- return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
- }
-}
-
-export const diffViewModelToken = define<DiffViewModel>('diff-view-model');
-
-export class DiffViewModel extends Model<DiffViewState | undefined> {
- constructor() {
- super(undefined);
- }
-}
diff --git a/polygerrit-ui/app/models/views/diff_test.ts b/polygerrit-ui/app/models/views/diff_test.ts
deleted file mode 100644
index 7fab2a4..0000000
--- a/polygerrit-ui/app/models/views/diff_test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {assert} from '@open-wc/testing';
-import {
- BasePatchSetNum,
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import '../../test/common-test-setup';
-import {createDiffUrl, DiffViewState} from './diff';
-
-suite('diff view state tests', () => {
- test('createDiffUrl', () => {
- const params: DiffViewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: 'x+y/path.cpp' as RepoName,
- patchNum: 12 as RevisionPatchSetNum,
- repo: '' as RepoName,
- };
- assert.equal(createDiffUrl(params), '/c/42/12/x%252By/path.cpp');
-
- window.CANONICAL_PATH = '/base';
- assert.equal(createDiffUrl(params).substring(0, 5), '/base');
- window.CANONICAL_PATH = undefined;
-
- params.repo = 'test' as RepoName;
- assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
-
- params.basePatchNum = 6 as BasePatchSetNum;
- assert.equal(createDiffUrl(params), '/c/test/+/42/6..12/x%252By/path.cpp');
-
- params.path = 'foo bar/my+file.txt%';
- params.patchNum = 2 as RevisionPatchSetNum;
- delete params.basePatchNum;
- assert.equal(
- createDiffUrl(params),
- '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'
- );
-
- params.path = 'file.cpp';
- params.lineNum = 123;
- assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#123');
-
- params.leftSide = true;
- assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#b123');
- });
-
- test('diff with repo name encoding', () => {
- const params: DiffViewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: 'x+y/path.cpp',
- patchNum: 12 as RevisionPatchSetNum,
- repo: 'x+/y' as RepoName,
- };
- assert.equal(createDiffUrl(params), '/c/x%252B/y/+/42/12/x%252By/path.cpp');
- });
-});
diff --git a/polygerrit-ui/app/models/views/edit.ts b/polygerrit-ui/app/models/views/edit.ts
deleted file mode 100644
index 3893576..0000000
--- a/polygerrit-ui/app/models/views/edit.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {
- EDIT,
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import {
- encodeURL,
- getBaseUrl,
- getPatchRangeExpression,
-} from '../../utils/url-util';
-import {define} from '../dependency';
-import {Model} from '../model';
-import {ViewState} from './base';
-
-export interface EditViewState extends ViewState {
- view: GerritView.EDIT;
- changeNum: NumericChangeId;
- repo: RepoName;
- path: string;
- patchNum: RevisionPatchSetNum;
- lineNum?: number;
-}
-
-export function createEditUrl(state: Omit<EditViewState, 'view'>): string {
- if (state.patchNum === undefined) {
- state = {...state, patchNum: EDIT};
- }
- let range = getPatchRangeExpression(state);
- if (range.length) range = '/' + range;
-
- let suffix = `${range}/${encodeURL(state.path || '', true)}`;
- suffix += ',edit';
-
- if (state.lineNum) {
- suffix += '#';
- suffix += state.lineNum;
- }
-
- if (state.repo) {
- const encodedProject = encodeURL(state.repo, true);
- return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
- } else {
- return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
- }
-}
-
-export const editViewModelToken = define<EditViewModel>('edit-view-model');
-
-export class EditViewModel extends Model<EditViewState | undefined> {
- constructor() {
- super(undefined);
- }
-}
diff --git a/polygerrit-ui/app/models/views/edit_test.ts b/polygerrit-ui/app/models/views/edit_test.ts
deleted file mode 100644
index 00bc805..0000000
--- a/polygerrit-ui/app/models/views/edit_test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {assert} from '@open-wc/testing';
-import {
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import '../../test/common-test-setup';
-import {createEditUrl, EditViewState} from './edit';
-
-suite('edit view state tests', () => {
- test('createEditUrl', () => {
- const params: EditViewState = {
- view: GerritView.EDIT,
- changeNum: 42 as NumericChangeId,
- repo: 'test-project' as RepoName,
- path: 'x+y/path.cpp' as RepoName,
- patchNum: 12 as RevisionPatchSetNum,
- lineNum: 31,
- };
- assert.equal(
- createEditUrl(params),
- '/c/test-project/+/42/12/x%252By/path.cpp,edit#31'
- );
-
- window.CANONICAL_PATH = '/base';
- assert.equal(createEditUrl(params).substring(0, 5), '/base');
- window.CANONICAL_PATH = undefined;
- });
-});
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 1857bad..14fb253 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -50,12 +50,10 @@
agreementViewModelToken,
} from '../models/views/agreement';
import {ChangeViewModel, changeViewModelToken} from '../models/views/change';
-import {DiffViewModel, diffViewModelToken} from '../models/views/diff';
import {
DocumentationViewModel,
documentationViewModelToken,
} from '../models/views/documentation';
-import {EditViewModel, editViewModelToken} from '../models/views/edit';
import {GroupViewModel, groupViewModelToken} from '../models/views/group';
import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
@@ -112,9 +110,7 @@
[agreementViewModelToken, () => new AgreementViewModel()],
[changeViewModelToken, () => new ChangeViewModel()],
[dashboardViewModelToken, () => new DashboardViewModel()],
- [diffViewModelToken, () => new DiffViewModel()],
[documentationViewModelToken, () => new DocumentationViewModel()],
- [editViewModelToken, () => new EditViewModel()],
[groupViewModelToken, () => new GroupViewModel()],
[pluginViewModelToken, () => new PluginViewModel()],
[repoViewModelToken, () => new RepoViewModel()],
@@ -139,9 +135,7 @@
resolver(agreementViewModelToken),
resolver(changeViewModelToken),
resolver(dashboardViewModelToken),
- resolver(diffViewModelToken),
resolver(documentationViewModelToken),
- resolver(editViewModelToken),
resolver(groupViewModelToken),
resolver(pluginViewModelToken),
resolver(repoViewModelToken),
@@ -154,7 +148,8 @@
changeModelToken,
() =>
new ChangeModel(
- resolver(routerModelToken),
+ resolver(navigationToken),
+ resolver(changeViewModelToken),
appContext.restApiService,
resolver(userModelToken)
),
@@ -163,7 +158,7 @@
commentsModelToken,
() =>
new CommentsModel(
- resolver(routerModelToken),
+ resolver(changeViewModelToken),
resolver(changeModelToken),
resolver(accountsModelToken),
appContext.restApiService,
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 572e107..2a5dff2 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -22,4 +22,5 @@
SUGGEST_EDIT = 'UiFeature__suggest_edit',
MENTION_USERS = 'UiFeature__mention_users',
RENDER_MARKDOWN = 'UiFeature__render_markdown',
+ REBASE_CHAIN = 'UiFeature__rebase_chain',
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 746ecf3..0d0c88f 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -143,7 +143,7 @@
import {ParsedChangeInfo} from '../../types/types';
import {ErrorCallback} from '../../api/rest';
import {addDraftProp, DraftInfo} from '../../utils/comment-util';
-import {BaseScheduler} from '../scheduler/scheduler';
+import {BaseScheduler, Scheduler} from '../scheduler/scheduler';
import {MaxInFlightScheduler} from '../scheduler/max-in-flight-scheduler';
import {escapeAndWrapSearchOperatorValue} from '../../utils/string-util';
@@ -270,6 +270,11 @@
function createWriteScheduler() {
return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 5);
}
+
+function createSerializingScheduler() {
+ return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 1);
+}
+
export class GrRestApiServiceImpl implements RestApiService, Finalizable {
readonly _cache = siteBasedCache; // Shared across instances.
@@ -286,6 +291,9 @@
// Private, but used in tests.
readonly _restApiHelper: GrRestApiHelper;
+ // Used to serialize requests for certain RPCs
+ readonly _serialScheduler: Scheduler<Response>;
+
constructor(private readonly authService: AuthService) {
this._restApiHelper = new GrRestApiHelper(
this._cache,
@@ -294,6 +302,7 @@
createReadScheduler(),
createWriteScheduler()
);
+ this._serialScheduler = createSerializingScheduler();
}
finalize() {}
@@ -2232,11 +2241,13 @@
return this.getFromProjectLookup(changeNum).then(project => {
const encodedRepoName = project ? encodeURIComponent(project) + '~' : '';
const url = `/accounts/self/starred.changes/${encodedRepoName}${changeNum}`;
- return this._restApiHelper.send({
- method: starred ? HttpMethod.PUT : HttpMethod.DELETE,
- url,
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
+ return this._serialScheduler.schedule(() =>
+ this._restApiHelper.send({
+ method: starred ? HttpMethod.PUT : HttpMethod.DELETE,
+ url,
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ })
+ );
});
}
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index edde7a4..c3c1cb6 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable} from 'rxjs';
-import {
- NumericChangeId,
- RevisionPatchSetNum,
- BasePatchSetNum,
-} from '../../types/common';
import {Model} from '../../models/model';
import {select} from '../../utils/observable-util';
import {define} from '../../models/dependency';
@@ -18,9 +13,7 @@
AGREEMENTS = 'agreements',
CHANGE = 'change',
DASHBOARD = 'dashboard',
- DIFF = 'diff',
DOCUMENTATION_SEARCH = 'documentation-search',
- EDIT = 'edit',
GROUP = 'group',
PLUGIN_SCREEN = 'plugin-screen',
REPO = 'repo',
@@ -31,9 +24,6 @@
export interface RouterState {
// Note that this router model view must be updated before view model state.
view?: GerritView;
- changeNum?: NumericChangeId;
- patchNum?: RevisionPatchSetNum;
- basePatchNum?: BasePatchSetNum;
}
export const routerModelToken = define<RouterModel>('router-model');
@@ -43,17 +33,6 @@
state => state.view
);
- readonly routerChangeNum$: Observable<NumericChangeId | undefined> = select(
- this.state$,
- state => state.changeNum
- );
-
- readonly routerPatchNum$: Observable<RevisionPatchSetNum | undefined> =
- select(this.state$, state => state.patchNum);
-
- readonly routerBasePatchNum$: Observable<BasePatchSetNum | undefined> =
- select(this.state$, state => state.basePatchNum);
-
constructor() {
super({});
}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 23a5794..f0a4cbe 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -105,7 +105,6 @@
import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
import {
DetailedLabelInfo,
- FileInfo,
QuickLabelInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
@@ -115,8 +114,8 @@
import {Category, RunStatus} from '../api/checks';
import {DiffInfo} from '../api/diff';
import {SearchViewState} from '../models/views/search';
-import {ChangeViewState} from '../models/views/change';
-import {EditViewState} from '../models/views/edit';
+import {ChangeChildView, ChangeViewState} from '../models/views/change';
+import {NormalizedFileInfo} from '../models/change/files-model';
const TEST_DEFAULT_EXPRESSION = 'label:Verified=MAX -label:Verified=MIN';
export const TEST_PROJECT_NAME: RepoName = 'test-project' as RepoName;
@@ -400,10 +399,15 @@
return messages;
}
-export function createFileInfo(): FileInfo {
+export function createFileInfo(
+ path = 'test-path/test-file.txt'
+): NormalizedFileInfo {
return {
size: 314,
size_delta: 7,
+ lines_deleted: 0,
+ lines_inserted: 0,
+ __path: path,
};
}
@@ -701,6 +705,7 @@
export function createChangeViewState(): ChangeViewState {
return {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
changeNum: TEST_NUMERIC_CHANGE_ID,
repo: TEST_PROJECT_NAME,
};
@@ -716,12 +721,22 @@
};
}
-export function createEditViewState(): EditViewState {
+export function createEditViewState(): ChangeViewState {
return {
- view: GerritView.EDIT,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
changeNum: TEST_NUMERIC_CHANGE_ID,
patchNum: EDIT,
- path: 'foo/bar.baz',
+ repo: TEST_PROJECT_NAME,
+ editView: {path: 'foo/bar.baz'},
+ };
+}
+
+export function createDiffViewState(): ChangeViewState {
+ return {
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.DIFF,
+ changeNum: TEST_NUMERIC_CHANGE_ID,
repo: TEST_PROJECT_NAME,
};
}
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 8d55e36..557e3a0 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -10,7 +10,6 @@
AccountInfo,
BasePatchSetNum,
ChangeViewChangeInfo,
- CommitId,
CommitInfo,
EditPatchSet,
PatchSetNum,
@@ -28,11 +27,6 @@
requestAvailability(): void;
}
-export interface CommitRange {
- baseCommit: CommitId;
- commit: CommitId;
-}
-
export type {CoverageRange} from '../api/diff';
export {CoverageType} from '../api/diff';
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 8af5beb..a92f0f8 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -598,3 +598,17 @@
.includes(account.email)
);
}
+
+export function findComment(
+ comments: {
+ [path: string]: (CommentInfo | DraftInfo)[];
+ },
+ commentId: UrlEncodedCommentId
+) {
+ if (!commentId) return undefined;
+ let comment;
+ for (const path of Object.keys(comments)) {
+ comment = comment || comments[path].find(c => c.id === commentId);
+ }
+ return comment;
+}
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index 1116123..b007d47 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {SpecialFilePath, FileInfoStatus} from '../constants/constants';
-import {FileInfo} from '../types/common';
+import {FileInfo, FileNameToFileInfoMap} from '../types/common';
import {hasOwnProperty} from './common-util';
export function specialFilePathCompare(a: string, b: string) {
@@ -55,7 +55,7 @@
// In case there are files with comments on them but they are unchanged, then
// we explicitly displays the file to render the comments with Unchanged status
export function addUnmodifiedFiles(
- files: {[filename: string]: FileInfo},
+ files: FileNameToFileInfoMap,
commentedPaths: {[fileName: string]: boolean}
) {
if (!commentedPaths) return;
diff --git a/polygerrit-ui/app/utils/path-list-util_test.ts b/polygerrit-ui/app/utils/path-list-util_test.ts
index 50f5c0e..3c9e0d3 100644
--- a/polygerrit-ui/app/utils/path-list-util_test.ts
+++ b/polygerrit-ui/app/utils/path-list-util_test.ts
@@ -12,9 +12,9 @@
specialFilePathCompare,
truncatePath,
} from './path-list-util';
-import {FileInfo} from '../api/rest-api';
import {hasOwnProperty} from './common-util';
import {assert} from '@open-wc/testing';
+import {FileNameToFileInfoMap} from '../types/common';
suite('path-list-utl tests', () => {
test('special sort', () => {
@@ -117,7 +117,7 @@
'file1.txt': true,
};
- const files: {[filename: string]: FileInfo} = {
+ const files: FileNameToFileInfoMap = {
'file2.txt': {
status: FileInfoStatus.REWRITTEN,
size_delta: 10,
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 2f9561b..8e97ba7 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -39,6 +39,7 @@
cpp = text/x-c++src
cql = text/x-cassandra
cxx = text/x-c++src
+cu = text/x-c++src
cyp = application/x-cypher-query
cypher = application/x-cypher-query
c++ = text/x-c++src