Merge changes I119f7594,I2c8398d6 * changes: Revert "Add project permission for removing votes/labels." Revert "Fix ChangeInfoDiffer.getAddedForMap method"
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 950049b..359361c 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -7189,11 +7189,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). @@ -8226,6 +8231,7 @@ ancestry, where the oldest ancestor is the first. |`contains_git_conflicts` ||Whether any of the rebased changes has conflicts due to rebasing. +|=========================== [[related-change-and-commit-info]] === RelatedChangeAndCommitInfo
diff --git a/contrib/git-gc-preserve b/contrib/git-gc-preserve index f3946ab..a886721 100755 --- a/contrib/git-gc-preserve +++ b/contrib/git-gc-preserve
@@ -67,6 +67,7 @@ test -f "$LOCKFILE" || touch "$LOCKFILE" exec 9> "$LOCKFILE" if flock -nx 9; then + echo -n "$$ $USERNAME@$HOSTNAME" >&9 trap unlock EXIT else echo "$0 is already running"
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..ef2c3f5 100644 --- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java +++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -16,6 +16,7 @@ import static com.google.gerrit.index.SchemaUtil.schema; +import com.google.common.collect.ImmutableList; import com.google.gerrit.index.Schema; import com.google.gerrit.index.SchemaDefinitions; @@ -31,14 +32,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.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.of(ProjectField.STATE_FIELD), + ImmutableList.of(ProjectField.STATE_SPEC)); // Bump Lucene version requires reindexing @Deprecated static final Schema<ProjectData> V3 = schema(V2);
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/GetRelatedChangesUtil.java b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java index 27eeae1..9a75469 100644 --- a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java +++ b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
@@ -91,8 +91,7 @@ } private List<ChangeData> getUnsortedRelated( - ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange) - throws IOException, PermissionBackendException { + ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange) { Set<String> groups = getAllGroups(changeData.patchSets()); logger.atFine().log("groups = %s", groups); if (groups.isEmpty()) {
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/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java index 3585d13..1c89566f 100644 --- a/java/com/google/gerrit/server/index/change/ChangeField.java +++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -988,7 +988,7 @@ /** Serialized change object, used for pre-populating results. */ private static final TypeToken<Entities.Change> CHANGE_TYPE_TOKEN = - new TypeToken<Entities.Change>() { + new TypeToken<>() { private static final long serialVersionUID = 1L; }; @@ -1007,7 +1007,7 @@ /** Serialized approvals for the current patch set, used for pre-populating results. */ private static final TypeToken<Iterable<Entities.PatchSetApproval>> APPROVAL_TYPE_TOKEN = - new TypeToken<Iterable<Entities.PatchSetApproval>>() { + new TypeToken<>() { private static final long serialVersionUID = 1L; }; @@ -1300,7 +1300,7 @@ /** Serialized patch set object, used for pre-populating results. */ private static final TypeToken<Iterable<Entities.PatchSet>> PATCH_SET_TYPE_TOKEN = - new TypeToken<Iterable<Entities.PatchSet>>() { + new TypeToken<>() { private static final long serialVersionUID = 1L; }; @@ -1621,7 +1621,7 @@ /** Serialized submit requirements, used for pre-populating results. */ private static final TypeToken<Iterable<Cache.SubmitRequirementResultProto>> STORED_SUBMIT_REQUIREMENTS_TYPE_TOKEN = - new TypeToken<Iterable<Cache.SubmitRequirementResultProto>>() { + new TypeToken<>() { private static final long serialVersionUID = 1L; };
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java index 26477a4..8f5e36e 100644 --- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java +++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.server.index.change.ChangeField.CHANGE_SPEC; +import static com.google.gerrit.server.index.change.ChangeField.NUMERIC_ID_STR_SPEC; import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC; import com.google.common.annotations.VisibleForTesting; @@ -68,10 +69,13 @@ int pageSizeMultiplier, int limit, Set<String> fields) { - // Always include project since it is needed to load the change from NoteDb. - if (!fields.contains(CHANGE_SPEC.getName()) && !fields.contains(PROJECT_SPEC.getName())) { + // Always include project and change id since both are needed to load the change from NoteDb. + if (!fields.contains(CHANGE_SPEC.getName()) + && !(fields.contains(PROJECT_SPEC.getName()) + && fields.contains(NUMERIC_ID_STR_SPEC.getName()))) { fields = new HashSet<>(fields); fields.add(PROJECT_SPEC.getName()); + fields.add(NUMERIC_ID_STR_SPEC.getName()); } return QueryOptions.create(config, start, pageSize, pageSizeMultiplier, limit, fields); }
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/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java index 2b856fb..43a212c 100644 --- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java +++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -28,6 +28,7 @@ import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Multimaps; import com.google.common.collect.Streams; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Patch; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; @@ -66,6 +67,7 @@ import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.HistogramDiff; import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; @@ -76,6 +78,7 @@ /** Implementation of the {@link GitFileDiffCache} */ @Singleton public class GitFileDiffCacheImpl implements GitFileDiffCache { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final String GIT_DIFF = "git_file_diff"; public static Module module() { @@ -340,8 +343,7 @@ throws IOException { if (!key.useTimeout()) { try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) { - FileHeader fileHeader = formatter.get().toFileHeader(diffEntry); - return GitFileDiff.create(diffEntry, fileHeader); + return GitFileDiff.create(diffEntry, getFileHeader(formatter, diffEntry)); } } // This submits the DiffFormatter to a different thread. The CloseablePool and our usage of it @@ -353,7 +355,7 @@ diffExecutor.submit( () -> { try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) { - return GitFileDiff.create(diffEntry, formatter.get().toFileHeader(diffEntry)); + return GitFileDiff.create(diffEntry, getFileHeader(formatter, diffEntry)); } }); try { @@ -385,6 +387,46 @@ ? diffEntry.getOldPath() : diffEntry.getNewPath(); } + + private FileHeader getFileHeader( + CloseablePool<DiffFormatter>.Handle formatter, DiffEntry diffEntry) throws IOException { + logger.atFine().log("getting file header for %s", formatDiffEntryForLogging(diffEntry)); + try { + return formatter.get().toFileHeader(diffEntry); + } catch (MissingObjectException e) { + throw new IOException( + String.format("Failed to get file header for %s", formatDiffEntryForLogging(diffEntry)), + e); + } + } + + private String formatDiffEntryForLogging(DiffEntry diffEntry) { + StringBuilder buf = new StringBuilder(); + buf.append("DiffEntry["); + buf.append(diffEntry.getChangeType()); + buf.append(" "); + switch (diffEntry.getChangeType()) { + case ADD: + buf.append(String.format("%s (%s)", diffEntry.getNewPath(), diffEntry.getNewId().name())); + break; + case COPY: + case RENAME: + buf.append( + String.format( + "%s (%s) -> %s (%s)", + diffEntry.getOldPath(), + diffEntry.getOldId().name(), + diffEntry.getNewPath(), + diffEntry.getNewId().name())); + break; + case DELETE: + case MODIFY: + buf.append(String.format("%s (%s)", diffEntry.getOldPath(), diffEntry.getOldId().name())); + break; + } + buf.append("]"); + return buf.toString(); + } } /**
diff --git a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java index 85a3ab9..df2e1cf 100644 --- a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java +++ b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
@@ -92,7 +92,6 @@ } @Override - @SuppressWarnings("CheckReturnValue") public void run() { logger.atFine().log("Loading project_list cache"); cache.refreshProjectList();
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/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/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java index 78615bf..fc4df49 100644 --- a/java/com/google/gerrit/server/query/change/ChangeData.java +++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -966,7 +966,7 @@ * submit requirements are evaluated online. * * <p>For changes loaded from the index, the value will be set from index field {@link - * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS}. + * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS_FIELD}. */ public Map<SubmitRequirement, SubmitRequirementResult> submitRequirements() { if (submitRequirements == null) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index 24d205d..da75057 100644 --- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -694,7 +694,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); } @@ -775,7 +776,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()); @@ -1140,7 +1141,10 @@ @Operator public Predicate<ChangeData> message(String text) throws QueryParseException { if (text.startsWith("^")) { - checkFieldAvailable(ChangeField.COMMIT_MESSAGE_EXACT, "messageexact"); + if (!args.index.getSchema().hasField(ChangeField.COMMIT_MESSAGE_EXACT)) { + throw new QueryParseException( + "'message' operator with regular expression is not supported on this gerrit host"); + } return new RegexMessagePredicate(text); } return ChangePredicates.message(text); @@ -1645,7 +1649,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/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java index 9a6b03e..259e71d 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/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java index 4248ac5..74bd94e 100644 --- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java +++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -844,9 +844,11 @@ public void setUp() throws Exception { init( id -> { + @SuppressWarnings("unused") Object unused = gApi.changes().id(id).rebaseChain(); }, (id, in) -> { + @SuppressWarnings("unused") Object unused = gApi.changes().id(id).rebaseChain(in); }); }
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/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/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/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java index 65eb3b8..a40afe8 100644 --- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java +++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -40,8 +40,7 @@ String metaId = "0e39795bb25dc914118224995c53c5c36923a461"; account.setMetaId(metaId); Iterable<byte[]> refStates = - (Iterable<byte[]>) - AccountField.REF_STATE_SPEC.get(AccountState.forAccount(account.build())); + AccountField.REF_STATE_SPEC.get(AccountState.forAccount(account.build())); List<String> values = toStrings(refStates); String expectedValue = allUsersName.get() + ":" + RefNames.refsUsers(account.id()) + ":" + metaId;
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java index 8919e04..fbf9c87 100644 --- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java +++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -46,6 +46,7 @@ import com.google.common.collect.Multimaps; import com.google.common.collect.Streams; import com.google.common.truth.ThrowableSubject; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gerrit.acceptance.ExtensionRegistry; import com.google.gerrit.acceptance.ExtensionRegistry.Registration; import com.google.gerrit.acceptance.FakeSubmitRule; @@ -109,12 +110,14 @@ import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.ListGroupMembership; import com.google.gerrit.server.account.VersionedAccountQueries; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.account.externalids.ExternalIdFactory; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ChangeTriplet; import com.google.gerrit.server.change.NotifyResolver; import com.google.gerrit.server.change.PatchSetInserter; import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.gerrit.server.group.testing.TestGroupBackend; import com.google.gerrit.server.index.change.ChangeField; @@ -134,8 +137,6 @@ import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gerrit.server.util.time.TimeUtil; import com.google.gerrit.testing.GerritServerTests; -import com.google.gerrit.testing.InMemoryRepositoryManager; -import com.google.gerrit.testing.InMemoryRepositoryManager.Repo; import com.google.gerrit.testing.TestChanges; import com.google.gerrit.testing.TestTimeUtil; import com.google.inject.Inject; @@ -160,6 +161,7 @@ import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.util.SystemReader; @@ -184,7 +186,7 @@ @Inject protected ChangeIndexer indexer; @Inject protected ExtensionRegistry extensionRegistry; @Inject protected IndexConfig indexConfig; - @Inject protected InMemoryRepositoryManager repoManager; + @Inject protected GitRepositoryManager repoManager; @Inject protected Provider<AnonymousUser> anonymousUserProvider; @Inject protected Provider<InternalChangeQuery> queryProvider; @Inject protected ChangeNotes.Factory notesFactory; @@ -207,11 +209,20 @@ protected Injector injector; protected LifecycleManager lifecycle; + + /** + * Index tests should not use username in query assert, since some backends do not use {@link + * ExternalId#SCHEME_USERNAME} + */ protected Account.Id userId; + protected CurrentUser user; + protected Account userAccount; private String systemTimeZone; + protected TestRepository<Repository> repo; + protected abstract Injector createInjector(); @Before @@ -227,6 +238,10 @@ @After public void cleanUp() { + if (repo != null) { + repo.close(); + repo = null; + } lifecycle.stop(); } @@ -253,8 +268,9 @@ return () -> requestUser; } - protected void resetUser() { + protected void resetUser() throws ConfigInvalidException, IOException { user = userFactory.create(userId); + userAccount = accounts.get(userId).get().account(); requestContext.setContext(newRequestContext(userId)); } @@ -290,9 +306,9 @@ @Test public void byId() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); assertQuery("12345"); assertQuery(change1.getId().get(), change1); @@ -301,8 +317,8 @@ @Test public void byKey() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); String key = change.getKey().get(); assertQuery("I0000000000000000000000000000000000000000"); @@ -314,8 +330,8 @@ @Test public void byTriplet() throws Exception { - TestRepository<Repo> repo = createProject("iabcde"); - Change change = insert(repo, newChangeForBranch(repo, "branch")); + repo = createAndOpenProject("iabcde"); + Change change = insert("iabcde", newChangeForBranch(repo, "branch")); String k = change.getKey().get(); assertQuery("iabcde~branch~" + k, change); @@ -337,11 +353,11 @@ @Test public void byStatus() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW); - Change change1 = insert(repo, ins1); + Change change1 = insert("repo", ins1); ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED); - Change change2 = insert(repo, ins2); + Change change2 = insert("repo", ins2); assertQuery("status:new", change1); assertQuery("status:NEW", change1); @@ -356,11 +372,11 @@ @Test public void byStatusOr() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW); - Change change1 = insert(repo, ins1); + Change change1 = insert("repo", ins1); ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED); - Change change2 = insert(repo, ins2); + Change change2 = insert("repo", ins2); assertQuery("status:new OR status:merged", change2, change1); assertQuery("status:new or status:merged", change2, change1); @@ -368,10 +384,10 @@ @Test public void byStatusOpen() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW); - Change change1 = insert(repo, ins1); - insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); + Change change1 = insert("repo", ins1); + insert("repo", newChangeWithStatus(repo, Change.Status.MERGED)); Change[] expected = new Change[] {change1}; assertQuery("status:open", expected); @@ -390,12 +406,12 @@ @Test public void byStatusClosed() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED); - Change change1 = insert(repo, ins1); + Change change1 = insert("repo", ins1); ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED); - Change change2 = insert(repo, ins2); - insert(repo, newChangeWithStatus(repo, Change.Status.NEW)); + Change change2 = insert("repo", ins2); + insert("repo", newChangeWithStatus(repo, Change.Status.NEW)); Change[] expected = new Change[] {change2, change1}; assertQuery("status:closed", expected); @@ -411,12 +427,12 @@ @Test public void byStatusAbandoned() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED); - insert(repo, ins1); + insert("repo", ins1); ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED); - Change change1 = insert(repo, ins2); - insert(repo, newChangeWithStatus(repo, Change.Status.NEW)); + Change change1 = insert("repo", ins2); + insert("repo", newChangeWithStatus(repo, Change.Status.NEW)); assertQuery("status:abandoned", change1); assertQuery("status:ABANDONED", change1); @@ -425,10 +441,10 @@ @Test public void byStatusPrefix() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW); - Change change1 = insert(repo, ins1); - insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); + Change change1 = insert("repo", ins1); + Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED)); assertQuery("status:n", change1); assertQuery("status:ne", change1); @@ -436,6 +452,7 @@ assertQuery("status:N", change1); assertQuery("status:nE", change1); assertQuery("status:neW", change1); + assertQuery("status:m", change2); Exception thrown = assertThrows(BadRequestException.class, () -> assertQuery("status:newx")); assertThat(thrown).hasMessageThat().isEqualTo("Unrecognized value: newx"); thrown = assertThrows(BadRequestException.class, () -> assertQuery("status:nx")); @@ -444,11 +461,11 @@ @Test public void byPrivate() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - Change change2 = insert(repo, newChange(repo), user2); + Change change2 = insert("repo", newChange(repo), user2); // No private changes. assertQuery("is:open", change2, change1); @@ -468,8 +485,8 @@ @Test public void byWip() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); assertQuery("is:open", change1); assertQuery("is:wip"); @@ -486,8 +503,8 @@ @Test public void excludeWipChangeFromReviewersDashboards() throws Exception { Account.Id user1 = createAccount("user1"); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWorkInProgress(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWorkInProgress(repo), userId); assertQuery("is:wip", change1); assertQuery("reviewer:" + user1); @@ -503,8 +520,8 @@ @Test public void byStarted() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWorkInProgress(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWorkInProgress(repo)); assertQuery("is:started"); @@ -539,11 +556,11 @@ @Test public void restorePendingReviewers() throws Exception { Project.NameKey project = Project.nameKey("repo"); - TestRepository<Repo> repo = createProject(project.get()); + repo = createAndOpenProject(project.get()); ConfigInput conf = new ConfigInput(); conf.enableReviewerByEmail = InheritableBoolean.TRUE; gApi.projects().name(project.get()).config(conf); - Change change1 = insert(repo, newChangeWorkInProgress(repo)); + Change change1 = insert("repo", newChangeWorkInProgress(repo)); Account.Id user1 = createAccount("user1"); Account.Id user2 = createAccount("user2"); String email1 = "email1@example.com"; @@ -596,9 +613,9 @@ @Test public void byCommit() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins = newChange(repo); - Change change = insert(repo, ins); + Change change = insert("repo", ins); String sha = ins.getCommitId().name(); assertQuery("0000000000000000000000000000000000000000"); @@ -612,11 +629,11 @@ @Test public void byOwner() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - Change change2 = insert(repo, newChange(repo), user2); + Change change2 = insert("repo", newChange(repo), user2); assertQuery("is:owner", change1); assertQuery("owner:" + userId.get(), change1); @@ -629,15 +646,16 @@ @Test public void byUploader() throws Exception { assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue(); - Account.Id user2 = - accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - CurrentUser user2CurrentUser = userFactory.create(user2); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); assertQuery("is:uploader", change1); assertQuery("uploader:" + userId.get(), change1); - change1 = newPatchSet(repo, change1, user2CurrentUser, /* message= */ Optional.empty()); + + Account.Id user2 = createAccount("anotheruser"); + CurrentUser user2CurrentUser = userFactory.create(user2); + + change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty()); // Uploader has changed assertQuery("uploader:" + userId.get()); assertQuery("uploader:" + user2.get(), change1); @@ -670,7 +688,7 @@ } private void byAuthorOrCommitterExact(String searchOperator) throws Exception { - TestRepository<Repo> repo = createProject("repo"); + createProject("repo"); PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com"); PersonIdent john = new PersonIdent("John", "john@example.com"); PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com"); @@ -678,10 +696,10 @@ PersonIdent myself = new PersonIdent("I Am", ua.preferredEmail()); PersonIdent selfName = new PersonIdent("My Self", "my.self@example.com"); - Change change1 = createChange(repo, johnDoe); - Change change2 = createChange(repo, john); - Change change3 = createChange(repo, doeSmith); - createChange(repo, selfName); + Change change1 = createChange("repo", johnDoe); + Change change2 = createChange("repo", john); + Change change3 = createChange("repo", doeSmith); + createChange("repo", selfName); // Only email address. assertQuery(searchOperator + "john.doe@example.com", change1); @@ -706,19 +724,19 @@ assertQuery(searchOperator + "self"); // ':self' matches a change created with the current user's email address - Change change5 = createChange(repo, myself); + Change change5 = createChange("repo", myself); assertQuery(searchOperator + "me", change5); assertQuery(searchOperator + "self", change5); } private void byAuthorOrCommitterFullText(String searchOperator) throws Exception { - TestRepository<Repo> repo = createProject("repo"); + createProject("repo"); PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com"); PersonIdent john = new PersonIdent("John", "john@example.com"); PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com"); - Change change1 = createChange(repo, johnDoe); - Change change2 = createChange(repo, john); - Change change3 = createChange(repo, doeSmith); + Change change1 = createChange("repo", johnDoe); + Change change2 = createChange("repo", john); + Change change3 = createChange("repo", doeSmith); // By exact name. assertQuery(searchOperator + "\"John Doe\"", change1); @@ -739,20 +757,25 @@ assertThat(thrown).hasMessageThat().contains("invalid value"); } - protected Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception { - RevCommit commit = - repo.parseBody(repo.commit().message("message").author(person).committer(person).create()); - return insert(repo, newChangeForCommit(repo, commit), null); + @CanIgnoreReturnValue + protected Change createChange(String repoName, PersonIdent person) throws Exception { + try (TestRepository<Repository> repo = + new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) { + RevCommit commit = + repo.parseBody( + repo.commit().message("message").author(person).committer(person).create()); + return insert("repo", newChangeForCommit(repo, commit), null); + } } @Test public void byOwnerIn() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - Change change2 = insert(repo, newChange(repo), user2); - Change change3 = insert(repo, newChange(repo), user2); + Change change2 = insert("repo", newChange(repo), user2); + Change change3 = insert("repo", newChange(repo), user2); gApi.changes().id(change3.getId().get()).current().review(ReviewInput.approve()); gApi.changes().id(change3.getId().get()).current().submit(); @@ -765,24 +788,25 @@ @Test public void byUploaderIn() throws Exception { assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); + assertQuery("uploaderin:Administrators", change1); - Account.Id user2 = - accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); + Account.Id user2 = createAccount("anotheruser"); CurrentUser user2CurrentUser = userFactory.create(user2); - change1 = newPatchSet(repo, change1, user2CurrentUser, /* message= */ Optional.empty()); + change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty()); + assertQuery("uploaderin:Administrators"); assertQuery("uploaderin:\"Registered Users\"", change1); } @Test public void byProject() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("project:foo"); assertQuery("project:repo"); @@ -792,16 +816,16 @@ @Test public void byProjectWithHidden() throws Exception { - TestRepository<Repo> hiddenProject = createProject("hiddenProject"); - insert(hiddenProject, newChange(hiddenProject)); + createProject("hiddenProject"); + insert("hiddenProject", newChange("hiddenProject")); projectOperations .project(Project.nameKey("hiddenProject")) .forUpdate() .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) .update(); - TestRepository<Repo> visibleProject = createProject("visibleProject"); - Change visibleChange = insert(visibleProject, newChange(visibleProject)); + createProject("visibleProject"); + Change visibleChange = insert("visibleProject", newChange("visibleProject")); assertQuery("project:visibleProject", visibleChange); assertQuery("project:hiddenProject"); assertQuery("project:visibleProject OR project:hiddenProject", visibleChange); @@ -809,13 +833,13 @@ @Test public void byParentOf() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create()); - Change change1 = insert(repo1, newChangeForCommit(repo1, commit1)); - RevCommit commit2 = repo1.parseBody(repo1.commit(commit1)); - Change change2 = insert(repo1, newChangeForCommit(repo1, commit2)); - RevCommit commit3 = repo1.parseBody(repo1.commit(commit1, commit2)); - Change change3 = insert(repo1, newChangeForCommit(repo1, commit3)); + repo = createAndOpenProject("repo1"); + RevCommit commit1 = repo.parseBody(repo.commit().message("message").create()); + Change change1 = insert("repo1", newChangeForCommit(repo, commit1)); + RevCommit commit2 = repo.parseBody(repo.commit(commit1)); + Change change2 = insert("repo1", newChangeForCommit(repo, commit2)); + RevCommit commit3 = repo.parseBody(repo.commit(commit1, commit2)); + Change change3 = insert("repo1", newChangeForCommit(repo, commit3)); assertQuery("parentof:" + change1.getId().get()); assertQuery("parentof:" + change1.getKey().get()); @@ -827,10 +851,10 @@ @Test public void byParentProject() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2", "repo1"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2", "repo1"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("parentproject:repo1", change2, change1); assertQuery("parentproject:repo2", change2); @@ -838,10 +862,10 @@ @Test public void byProjectPrefix() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("projects:foo"); assertQuery("projects:repo1", change1); @@ -851,10 +875,10 @@ @Test public void byRepository() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("repository:foo"); assertQuery("repository:repo"); @@ -864,10 +888,10 @@ @Test public void byParentRepository() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2", "repo1"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2", "repo1"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("parentrepository:repo1", change2, change1); assertQuery("parentrepository:repo2", change2); @@ -875,10 +899,10 @@ @Test public void byRepositoryPrefix() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("repositories:foo"); assertQuery("repositories:repo1", change1); @@ -888,10 +912,10 @@ @Test public void byRepo() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("repo:foo"); assertQuery("repo:repo"); @@ -901,10 +925,10 @@ @Test public void byParentRepo() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2", "repo1"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2", "repo1"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("parentrepo:repo1", change2, change1); assertQuery("parentrepo:repo2", change2); @@ -912,10 +936,10 @@ @Test public void byRepoPrefix() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change1 = insert(repo1, newChange(repo1)); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + createProject("repo2"); + Change change1 = insert("repo1", newChange("repo1")); + Change change2 = insert("repo2", newChange("repo2")); assertQuery("repos:foo"); assertQuery("repos:repo1", change1); @@ -925,9 +949,9 @@ @Test public void byBranchAndRef() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeForBranch(repo, "master")); - Change change2 = insert(repo, newChangeForBranch(repo, "branch")); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeForBranch(repo, "master")); + Change change2 = insert("repo", newChangeForBranch(repo, "branch")); assertQuery("branch:foo"); assertQuery("branch:master", change1); @@ -944,26 +968,26 @@ @Test public void byTopic() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithTopic(repo, "feature1"); - Change change1 = insert(repo, ins1); + Change change1 = insert("repo", ins1); ChangeInserter ins2 = newChangeWithTopic(repo, "feature2"); - Change change2 = insert(repo, ins2); + Change change2 = insert("repo", ins2); ChangeInserter ins3 = newChangeWithTopic(repo, "Cherrypick-feature2"); - Change change3 = insert(repo, ins3); + Change change3 = insert("repo", ins3); ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup"); - Change change4 = insert(repo, ins4); + Change change4 = insert("repo", ins4); ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local"); - Change change5 = insert(repo, ins5); + Change change5 = insert("repo", ins5); ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training"); - Change change6 = insert(repo, ins6); + Change change6 = insert("repo", ins6); - Change change_no_topic = insert(repo, newChange(repo)); + Change changeNoTopic = insert("repo", newChange(repo)); assertQuery("intopic:foo"); assertQuery("intopic:feature1", change1); @@ -972,8 +996,8 @@ assertQuery("intopic:feature2", change4, change3, change2); assertQuery("intopic:fixup", change4); assertQuery("intopic:gerrit", change6, change5); - assertQuery("topic:\"\"", change_no_topic); - assertQuery("intopic:\"\"", change_no_topic); + assertQuery("topic:\"\"", changeNoTopic); + assertQuery("intopic:\"\"", changeNoTopic); assume().that(getSchema().hasField(ChangeField.PREFIX_TOPIC)).isTrue(); assertQuery("prefixtopic:feature", change4, change2, change1); @@ -983,16 +1007,16 @@ @Test public void byTopicRegex() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChangeWithTopic(repo, "feature1"); - Change change1 = insert(repo, ins1); + Change change1 = insert("repo", ins1); ChangeInserter ins2 = newChangeWithTopic(repo, "Cherrypick-feature1"); - Change change2 = insert(repo, ins2); + Change change2 = insert("repo", ins2); ChangeInserter ins3 = newChangeWithTopic(repo, "feature1-fixup"); - Change change3 = insert(repo, ins3); + Change change3 = insert("repo", ins3); assertQuery("intopic:^feature1.*", change3, change1); assertQuery("intopic:{^.*feature1$}", change2, change1); @@ -1000,13 +1024,13 @@ @Test public void byMessageExact() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("one").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("two").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody(repo.commit().message("A great \"fix\" to my bug").create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); assertQuery("message:foo"); assertQuery("message:one", change1); @@ -1017,16 +1041,16 @@ @Test public void byMessageRegEx() throws Exception { assume().that(getSchema().hasField(ChangeField.COMMIT_MESSAGE_EXACT)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("aaaabcc").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("aaaacc").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody(repo.commit().message("Title\n\nHELLO WORLD").create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); RevCommit commit4 = repo.parseBody(repo.commit().message("Title\n\nfoobar hello WORLD").create()); - Change change4 = insert(repo, newChangeForCommit(repo, commit4)); + Change change4 = insert("repo", newChangeForCommit(repo, commit4)); assertQuery("message:\"^aaaa(b|c)*\"", change2, change1); assertQuery("message:\"^aaaa(c)*c.*\"", change2); @@ -1038,7 +1062,7 @@ @Test public void bySubject() throws Exception { assume().that(getSchema().hasField(ChangeField.SUBJECT_SPEC)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody( repo.commit() @@ -1047,7 +1071,7 @@ + "Message body\n\n" + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920") .create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody( repo.commit() @@ -1056,7 +1080,7 @@ + "Message body for another commit\n\n" + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921") .create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody( repo.commit() @@ -1065,7 +1089,7 @@ + "Last message body\n\n" + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921") .create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); assertQuery("subject:First", change1); assertQuery("subject:Second", change2); @@ -1075,7 +1099,7 @@ assertQuery("subject:body"); change1 = newPatchSet( - repo, + "repo", change1, user, Optional.of("Rework of commit with test subject\n\n" + "Message body\n\n")); @@ -1087,7 +1111,7 @@ @Test public void bySubjectPrefix() throws Exception { assume().that(getSchema().hasField(ChangeField.PREFIX_SUBJECT_SPEC)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody( repo.commit() @@ -1096,7 +1120,7 @@ + "Message body\n\n" + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920") .create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody( repo.commit() @@ -1105,7 +1129,7 @@ + "Message body for another commit\n\n" + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921") .create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody( repo.commit() @@ -1114,7 +1138,7 @@ + "Last message body\n\n" + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921") .create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); assertQuery("prefixsubject:\"[FOO\"", change3, change1); assertQuery("prefixsubject:\"[BAR\"", change2); @@ -1124,7 +1148,7 @@ assertQuery("prefixsubject:FOO"); change1 = newPatchSet( - repo, + "repo", change1, user, Optional.of("[BAR123] Rework of commit with test subject\n\n" + "Message body\n\n")); @@ -1134,11 +1158,11 @@ @Test public void fullTextWithNumbers() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("12345 67890").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("12346 67891").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); assertQuery("message:1234"); assertQuery("message:12345", change1); @@ -1147,13 +1171,13 @@ @Test public void fullTextMultipleTerms() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("Signed-off: owner").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("Signed by owner").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody(repo.commit().message("This change is off").create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); assertQuery("message:\"Signed-off: owner\"", change1); assertQuery("message:\"Signed\"", change2, change1); @@ -1162,11 +1186,11 @@ @Test public void byMessageMixedCase() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("Hello gerrit").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("Hello Gerrit").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); assertQuery("message:gerrit", change2, change1); assertQuery("message:Gerrit", change2, change1); @@ -1174,16 +1198,16 @@ @Test public void byMessageSubstring() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("https://gerrit.local").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); assertQuery("message:gerrit", change1); } @Test public void byLabel() throws Exception { - accountManager.authenticate(authRequestFactory.createForUser("anotheruser")); - TestRepository<Repo> repo = createProject("repo"); + Account.Id anotherUser = createAccount("anotheruser"); + repo = createAndOpenProject("repo"); ChangeInserter ins = newChange(repo); ChangeInserter ins2 = newChange(repo); ChangeInserter ins3 = newChange(repo); @@ -1191,24 +1215,24 @@ ChangeInserter ins5 = newChange(repo); ChangeInserter ins6 = newChange(repo); - Change reviewMinus2Change = insert(repo, ins); + Change reviewMinus2Change = insert("repo", ins); gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject()); - Change reviewMinus1Change = insert(repo, ins2); + Change reviewMinus1Change = insert("repo", ins2); gApi.changes().id(reviewMinus1Change.getId().get()).current().review(ReviewInput.dislike()); - Change noLabelChange = insert(repo, ins3); + Change noLabelChange = insert("repo", ins3); - Change reviewPlus1Change = insert(repo, ins4); + Change reviewPlus1Change = insert("repo", ins4); gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend()); - Change reviewTwoPlus1Change = insert(repo, ins5); + Change reviewTwoPlus1Change = insert("repo", ins5); gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend()); requestContext.setContext(newRequestContext(createAccount("user1"))); gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend()); requestContext.setContext(newRequestContext(userId)); - Change reviewPlus2Change = insert(repo, ins6); + Change reviewPlus2Change = insert("repo", ins6); gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve()); Map<String, Short> m = @@ -1272,9 +1296,15 @@ assertQuery("label:Code-Review<=-2", reviewMinus2Change); assertQuery("label:Code-Review<-2"); - assertQuery("label:Code-Review=+1,anotheruser"); - assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change); - assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change); + assertQuery("label:Code-Review=+1," + anotherUser); + assertQuery( + String.format("label:Code-Review=+1,%s", userAccount.preferredEmail()), + reviewTwoPlus1Change, + reviewPlus1Change); + assertQuery( + String.format("label:Code-Review=+1,user=%s", userAccount.preferredEmail()), + reviewTwoPlus1Change, + reviewPlus1Change); assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change); assertQuery( "label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change); @@ -1341,9 +1371,8 @@ @Test public void byLabelMulti() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Project.NameKey project = - Project.nameKey(repo.getRepository().getDescription().getRepositoryName()); + Project.NameKey project = Project.nameKey("repo"); + repo = createAndOpenProject(project.get()); LabelType verified = label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); @@ -1370,25 +1399,25 @@ ChangeInserter ins5 = newChange(repo); // CR+1 - Change reviewCRplus1 = insert(repo, ins); + Change reviewCRplus1 = insert(project.get(), ins); gApi.changes().id(reviewCRplus1.getId().get()).current().review(ReviewInput.recommend()); // CR+2 - Change reviewCRplus2 = insert(repo, ins2); + Change reviewCRplus2 = insert(project.get(), ins2); gApi.changes().id(reviewCRplus2.getId().get()).current().review(ReviewInput.approve()); // CR+1 VR+1 - Change reviewCRplus1VRplus1 = insert(repo, ins3); + Change reviewCRplus1VRplus1 = insert(project.get(), ins3); gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(ReviewInput.recommend()); gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(reviewVerified); // CR+2 VR+1 - Change reviewCRplus2VRplus1 = insert(repo, ins4); + Change reviewCRplus2VRplus1 = insert(project.get(), ins4); gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(ReviewInput.approve()); gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(reviewVerified); // VR+1 - Change reviewVRplus1 = insert(repo, ins5); + Change reviewVRplus1 = insert(project.get(), ins5); gApi.changes().id(reviewVRplus1.getId().get()).current().review(reviewVerified); assertQuery("label:Code-Review=+1", reviewCRplus1VRplus1, reviewCRplus1); @@ -1407,28 +1436,28 @@ @Test public void byLabelNotOwner() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins = newChange(repo); Account.Id user1 = createAccount("user1"); - Change reviewPlus1Change = insert(repo, ins); + Change reviewPlus1Change = insert("repo", ins); // post a review with user1 requestContext.setContext(newRequestContext(user1)); gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend()); - assertQuery("label:Code-Review=+1,user=user1", reviewPlus1Change); + assertQuery("label:Code-Review=+1,user=" + user1, reviewPlus1Change); assertQuery("label:Code-Review=+1,owner"); } @Test public void byLabelNonUploader() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins = newChange(repo); Account.Id user1 = createAccount("user1"); // create a change with "user" - Change reviewPlus1Change = insert(repo, ins); + Change reviewPlus1Change = insert("repo", ins); // add a +1 vote with "user". Query doesn't match since voter is the uploader. gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend()); @@ -1467,8 +1496,8 @@ @Test public void byLabelGroup() throws Exception { Account.Id user1 = createAccount("user1"); - createAccount("user2"); - TestRepository<Repo> repo = createProject("repo"); + Account.Id user2 = createAccount("user2"); + repo = createAndOpenProject("repo"); // create group and add users String g1 = createGroup("group1", "Administrators"); @@ -1477,7 +1506,7 @@ gApi.groups().id(g2).addMembers("user2"); // create a change - Change change1 = insert(repo, newChange(repo), user1); + Change change1 = insert("repo", newChange(repo), user1); // post a review with user1 requestContext.setContext(newRequestContext(user1)); @@ -1490,8 +1519,8 @@ requestContext.setContext(newRequestContext(userId)); assertQuery("label:Code-Review=+1,group1", change1); assertQuery("label:Code-Review=+1,group=group1", change1); - assertQuery("label:Code-Review=+1,user=user1", change1); - assertQuery("label:Code-Review=+1,user=user2"); + assertQuery("label:Code-Review=+1,user=" + user1, change1); + assertQuery("label:Code-Review=+1,user=" + user2); assertQuery("label:Code-Review=+1,group=group2"); } @@ -1499,7 +1528,7 @@ public void byLabelExternalGroup() throws Exception { Account.Id user1 = createAccount("user1"); Account.Id user2 = createAccount("user2"); - TestRepository<InMemoryRepositoryManager.Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); // create group and add users AccountGroup.UUID external_group1 = AccountGroup.uuid("testbackend:group1"); @@ -1511,8 +1540,8 @@ testGroupBackend.setMembershipsOf( user2, new ListGroupMembership(ImmutableList.of(external_group2))); - Change change1 = insert(repo, newChange(repo), user1); - Change change2 = insert(repo, newChange(repo), user1); + Change change1 = insert("repo", newChange(repo), user1); + Change change2 = insert("repo", newChange(repo), user1); // post a review with user1 and other_user requestContext.setContext(newRequestContext(user1)); @@ -1528,18 +1557,18 @@ assertQuery("label:Code-Review=+1," + external_group1.get(), change1); assertQuery("label:Code-Review=+1,group=" + external_group1.get(), change1); - assertQuery("label:Code-Review=+1,user=user1", change1); - assertQuery("label:Code-Review=+1,user=user2"); + assertQuery("label:Code-Review=+1,user=" + user1, change1); + assertQuery("label:Code-Review=+1,user=" + user2); assertQuery("label:Code-Review=+1,group=" + external_group2.get()); } @Test public void limit() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Change last = null; int n = 5; for (int i = 0; i < n; i++) { - last = insert(repo, newChange(repo)); + last = insert("repo", newChange(repo)); } for (int i = 1; i <= n + 2; i++) { @@ -1564,10 +1593,10 @@ @Test public void start() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); List<Change> changes = new ArrayList<>(); for (int i = 0; i < 2; i++) { - changes.add(insert(repo, newChange(repo))); + changes.add(insert("repo", newChange(repo))); } assertQuery("status:new", changes.get(1), changes.get(0)); @@ -1584,10 +1613,10 @@ @Test public void startWithLimit() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); List<Change> changes = new ArrayList<>(); for (int i = 0; i < 3; i++) { - changes.add(insert(repo, newChange(repo))); + changes.add(insert("repo", newChange(repo))); } assertQuery("status:new limit:2", changes.get(2), changes.get(1)); @@ -1598,8 +1627,8 @@ @Test public void maxPages() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); QueryRequest query = newQuery("status:new").withLimit(10); assertQuery(query, change); @@ -1614,12 +1643,12 @@ @Test public void updateOrder() throws Exception { resetTimeWithClockStep(2, MINUTES); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); List<ChangeInserter> inserters = new ArrayList<>(); List<Change> changes = new ArrayList<>(); for (int i = 0; i < 5; i++) { inserters.add(newChange(repo)); - changes.add(insert(repo, inserters.get(i))); + changes.add(insert("repo", inserters.get(i))); } for (int i : ImmutableList.of(2, 0, 1, 4, 3)) { @@ -1641,10 +1670,10 @@ @Test public void updatedOrder() throws Exception { resetTimeWithClockStep(1, SECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins1 = newChange(repo); - Change change1 = insert(repo, ins1); - Change change2 = insert(repo, newChange(repo)); + Change change1 = insert("repo", ins1); + Change change2 = insert("repo", newChange(repo)); assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2)); assertQuery("status:new", change2, change1); @@ -1662,12 +1691,12 @@ @Test public void filterOutMoreThanOnePageOfResults() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo), userId); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); for (int i = 0; i < 5; i++) { - insert(repo, newChange(repo), user2); + insert("repo", newChange(repo), user2); } assertQuery("status:new ownerin:Administrators", change); @@ -1676,11 +1705,11 @@ @Test public void filterOutAllResults() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); for (int i = 0; i < 5; i++) { - insert(repo, newChange(repo), user2); + insert("repo", newChange(repo), user2); } assertQuery("status:new ownerin:Administrators"); @@ -1689,8 +1718,8 @@ @Test public void byFileExact() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2")); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2")); assertQuery("file:file"); assertQuery("file:dir", change); @@ -1702,8 +1731,8 @@ @Test public void byFileRegex() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2")); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2")); assertQuery("file:.*file.*"); assertQuery("file:^file.*"); // Whole path only. @@ -1712,8 +1741,8 @@ @Test public void byPathExact() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2")); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2")); assertQuery("path:file"); assertQuery("path:dir"); @@ -1725,8 +1754,8 @@ @Test public void byPathRegex() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2")); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2")); assertQuery("path:.*file.*"); assertQuery("path:^dir.file.*", change); @@ -1734,12 +1763,12 @@ @Test public void byExtension() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc")); - Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC")); - Change change3 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc")); - Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java", "foo")); - Change change5 = insert(repo, newChangeWithFiles(repo, "foo")); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc")); + Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC")); + Change change3 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc")); + Change change4 = insert("repo", newChangeWithFiles(repo, "Quux.java", "foo")); + Change change5 = insert("repo", newChangeWithFiles(repo, "foo")); assertQuery("extension:java", change4); assertQuery("ext:java", change4); @@ -1755,14 +1784,14 @@ @Test public void byOnlyExtensions() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc")); - Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H")); - Change change3 = insert(repo, newChangeWithFiles(repo, "foo.CC", "bar.cc")); - Change change4 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc")); - Change change5 = insert(repo, newChangeWithFiles(repo, "Quux.java")); - Change change6 = insert(repo, newChangeWithFiles(repo, "foo.txt", "foo")); - Change change7 = insert(repo, newChangeWithFiles(repo, "foo")); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc")); + Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H")); + Change change3 = insert("repo", newChangeWithFiles(repo, "foo.CC", "bar.cc")); + Change change4 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc")); + Change change5 = insert("repo", newChangeWithFiles(repo, "Quux.java")); + Change change6 = insert("repo", newChangeWithFiles(repo, "foo.txt", "foo")); + Change change7 = insert("repo", newChangeWithFiles(repo, "foo")); // case doesn't matter assertQuery("onlyextensions:cc,h", change4, change2, change1); @@ -1802,23 +1831,23 @@ @Test public void byFooter() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create()); - Change change4 = insert(repo, newChangeForCommit(repo, commit4)); + Change change4 = insert("repo", newChangeForCommit(repo, commit4)); // create a changes with lines that look like footers, but which are not RevCommit commit5 = repo.parseBody( repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create()); - Change change5 = insert(repo, newChangeForCommit(repo, commit5)); + Change change5 = insert("repo", newChangeForCommit(repo, commit5)); RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create()); - insert(repo, newChangeForCommit(repo, commit6)); + insert("repo", newChangeForCommit(repo, commit6)); // matching by 'key=value' works assertQuery("footer:foo=bar", change3, change1); @@ -1852,15 +1881,15 @@ @Test public void byFooterName() throws Exception { assume().that(getSchema().hasField(ChangeField.FOOTER_NAME)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nBaR: baz").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); // create a changes with lines that look like footers, but which are not RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create()); - insert(repo, newChangeForCommit(repo, commit6)); + insert("repo", newChangeForCommit(repo, commit6)); // matching by 'key=value' works assertQuery("hasfooter:foo", change1); @@ -1872,14 +1901,14 @@ @Test public void byDirectory() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc")); - Change change2 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js")); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWithFiles(repo, "src/foo.h", "src/foo.cc")); + Change change2 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js")); Change change3 = - insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt")); - Change change4 = insert(repo, newChangeWithFiles(repo, "a.txt")); - Change change5 = insert(repo, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt")); - Change change6 = insert(repo, newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt")); + insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt")); + Change change4 = insert("repo", newChangeWithFiles(repo, "a.txt")); + Change change5 = insert("repo", newChangeWithFiles(repo, "a/b/c/d/e/foo.txt")); + Change change6 = insert("repo", newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt")); // matching by directory prefix works assertQuery("directory:src", change2, change1); @@ -1940,10 +1969,10 @@ @Test public void byDirectoryRegex() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js")); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js")); Change change2 = - insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt")); + insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt")); // match by regexp assertQuery("directory:^.*va.*", change1); @@ -1953,9 +1982,9 @@ @Test public void byComment() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ChangeInserter ins = newChange(repo); - Change change = insert(repo, ins); + Change change = insert("repo", ins); ReviewInput input = new ReviewInput(); input.message = "toplevel"; @@ -1983,11 +2012,11 @@ public void byAge() throws Exception { long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS); resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); long startMs = TestTimeUtil.START.toEpochMilli(); - Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs)); + Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs)); Change change2 = - insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs)); + insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs)); // Stop time so age queries use the same endpoint. TestTimeUtil.setClockStep(0, MILLISECONDS); @@ -2024,11 +2053,11 @@ public void byBeforeUntil() throws Exception { long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS); resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); long startMs = TestTimeUtil.START.toEpochMilli(); - Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs)); + Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs)); Change change2 = - insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs)); + insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs)); TestTimeUtil.setClockStep(0, MILLISECONDS); // Change1 was last updated on 2009-09-30 21:00:00 -0000 @@ -2076,11 +2105,11 @@ public void byAfterSince() throws Exception { long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS); resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); long startMs = TestTimeUtil.START.toEpochMilli(); - Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs)); + Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs)); Change change2 = - insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs)); + insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs)); TestTimeUtil.setClockStep(0, MILLISECONDS); // Change1 was last updated on 2009-09-30 21:00:00 -0000 @@ -2120,12 +2149,12 @@ // Stop the clock, will set time to specific test values. resetTimeWithClockStep(0, MILLISECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); long startMs = TestTimeUtil.START.toEpochMilli(); TestTimeUtil.setClock(new Timestamp(startMs)); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs)); submit(change3); @@ -2180,12 +2209,12 @@ // Stop the clock, will set time to specific test values. resetTimeWithClockStep(0, MILLISECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); long startMs = TestTimeUtil.START.toEpochMilli(); TestTimeUtil.setClock(new Timestamp(startMs)); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); assertThat(TimeUtil.nowMs()).isEqualTo(startMs); TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs)); @@ -2250,13 +2279,13 @@ // Stop the clock, will set time to specific test values. resetTimeWithClockStep(0, MILLISECONDS); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); long startMs = TestTimeUtil.START.toEpochMilli(); TestTimeUtil.setClock(new Timestamp(startMs)); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs)); submit(change2); @@ -2283,15 +2312,15 @@ @Test public void bySize() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); // added = 3, deleted = 0, delta = 3 RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "foo\n\foo\nfoo").create()); // added = 0, deleted = 2, delta = 2 RevCommit commit2 = repo.parseBody(repo.commit().parent(commit1).add("file1", "foo").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); assertQuery("added:>4"); assertQuery("-added:<=4"); @@ -2339,9 +2368,9 @@ } private List<Change> setUpHashtagChanges() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); addHashtags(change1.getId(), "foo", "aaa-bbb-ccc"); addHashtags(change2.getId(), "foo", "bar", "a tag", "ACamelCaseTag"); @@ -2388,10 +2417,10 @@ @Test public void byHashtagRegex() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); addHashtags(change1.getId(), "feature1"); addHashtags(change1.getId(), "trending"); addHashtags(change2.getId(), "Cherrypick-feature1"); @@ -2404,27 +2433,27 @@ @Test public void byDefault() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); - Change change1 = insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); RevCommit commit2 = repo.parseBody(repo.commit().message("foosubject").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody(repo.commit().add("Foo.java", "foo contents").create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); ChangeInserter ins4 = newChange(repo); - Change change4 = insert(repo, ins4); + Change change4 = insert("repo", ins4); ReviewInput ri4 = new ReviewInput(); ri4.message = "toplevel"; ri4.labels = ImmutableMap.of("Code-Review", (short) 1); gApi.changes().id(change4.getId().get()).current().review(ri4); ChangeInserter ins5 = newChangeWithTopic(repo, "feature5"); - Change change5 = insert(repo, ins5); + Change change5 = insert("repo", ins5); - Change change6 = insert(repo, newChangeForBranch(repo, "branch6")); + Change change6 = insert("repo", newChangeForBranch(repo, "branch6")); assertQuery(change1.getId().get(), change1); assertQuery(ChangeTriplet.format(change1), change1); @@ -2445,18 +2474,18 @@ @Test public void byDefaultWithCommitPrefix() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit = repo.parseBody(repo.commit().message("message").create()); - Change change = insert(repo, newChangeForCommit(repo, commit)); + Change change = insert("repo", newChangeForCommit(repo, commit)); assertQuery(commit.getId().getName().substring(0, 6), change); } @Test public void visible() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChangePrivate(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChangePrivate(repo)); String q = "project:repo"; @@ -2510,8 +2539,8 @@ // Switch to user3 requestContext.setContext(newRequestContext(user3)); - Change change3 = insert(repo, newChange(repo), user3); - Change change4 = insert(repo, newChangePrivate(repo), user3); + Change change3 = insert("repo", newChange(repo), user3); + Change change4 = insert("repo", newChangePrivate(repo), user3); // User3 can see both their changes and the first user's change assertQuery(q + " visibleto:" + user3.get(), change4, change3, change1); @@ -2551,9 +2580,9 @@ @Test public void visibleToSelf() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); gApi.changes().id(change2.getChangeId()).setPrivate(true, "private"); @@ -2569,16 +2598,12 @@ @Test public void byCommentBy() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - int user2 = - accountManager - .authenticate(authRequestFactory.createForUser("anotheruser")) - .getAccountId() - .get(); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Account.Id user2 = createAccount("anotheruser"); ReviewInput input = new ReviewInput(); input.message = "toplevel"; ReviewInput.CommentInput comment = new ReviewInput.CommentInput(); @@ -2599,8 +2624,8 @@ public void bySubmitRuleResult() throws Exception { try (Registration registration = extensionRegistry.newRegistration().add(new FakeSubmitRule())) { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); // The fake submit rule exports its ruleName as "FakeSubmitRule" assertQuery("rule:FakeSubmitRule"); @@ -2619,17 +2644,17 @@ public void byNonExistingSubmitRule_returnsEmpty() throws Exception { try (Registration registration = extensionRegistry.newRegistration().add(new FakeSubmitRule())) { - TestRepository<Repo> repo = createProject("repo"); - insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + insert("repo", newChange(repo)); assertQuery("rule:non-existent-rule"); } } @Test public void byHasDraft() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); assertQuery("has:draft"); @@ -2661,8 +2686,8 @@ */ public void byHasDraftExcludesZombieDrafts() throws Exception { Project.NameKey project = Project.nameKey("repo"); - TestRepository<Repo> repo = createProject(project.get()); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject(project.get()); + Change change = insert("repo", newChange(repo)); Change.Id id = change.getId(); DraftInput in = new DraftInput(); @@ -2674,7 +2699,7 @@ assertQuery("has:draft", change); assertQuery("commentby:" + userId); - try (TestRepository<Repo> allUsers = + try (TestRepository<Repository> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName))) { Ref draftsRef = allUsers.getRepository().exactRef(RefNames.refsDraftComments(id, userId)); assertThat(draftsRef).isNotNull(); @@ -2698,15 +2723,15 @@ @Test public void byHasDraftWithManyDrafts() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Change[] changesWithDrafts = new Change[30]; // unrelated change not shown in the result. - insert(repo, newChange(repo)); + insert("repo", newChange(repo)); for (int i = 0; i < changesWithDrafts.length; i++) { // put the changes in reverse order since this is the order we receive them from the index. - changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo)); + changesWithDrafts[changesWithDrafts.length - 1 - i] = insert("repo", newChange(repo)); DraftInput in = new DraftInput(); in.line = 1; in.message = "nit: trailing whitespace"; @@ -2726,10 +2751,10 @@ @Test public void byStarredBy() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + insert("repo", newChange(repo)); gApi.accounts().self().starChange(change1.getId().toString()); gApi.accounts().self().starChange(change2.getId().toString()); @@ -2745,8 +2770,8 @@ @Test public void byStar() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED)); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); @@ -2761,11 +2786,11 @@ @Test public void byStarWithManyStars() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Change[] changesWithDrafts = new Change[30]; for (int i = 0; i < changesWithDrafts.length; i++) { // put the changes in reverse order since this is the order we receive them from the index. - changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo)); + changesWithDrafts[changesWithDrafts.length - 1 - i] = insert("repo", newChange(repo)); // star the change gApi.accounts() @@ -2779,12 +2804,12 @@ @Test public void byFrom() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - Change change2 = insert(repo, newChange(repo), user2); + Change change2 = insert("repo", newChange(repo), user2); ReviewInput input = new ReviewInput(); input.message = "toplevel"; @@ -2800,7 +2825,7 @@ @Test public void conflicts() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody( repo.commit() @@ -2812,10 +2837,10 @@ RevCommit commit3 = repo.parseBody(repo.commit().add("dir/file2", "contents2 different").create()); RevCommit commit4 = repo.parseBody(repo.commit().add("file4", "contents4").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); - Change change4 = insert(repo, newChangeForCommit(repo, commit4)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); + Change change4 = insert("repo", newChangeForCommit(repo, commit4)); assertQuery("conflicts:" + change1.getId().get(), change3); assertQuery("conflicts:" + change2.getId().get()); @@ -2828,11 +2853,12 @@ name = "change.mergeabilityComputationBehavior", value = "API_REF_UPDATED_AND_CHANGE_REINDEX") public void mergeable() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + assume().that(getSchema().hasField(ChangeField.MERGEABLE_SPEC)).isTrue(); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create()); RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); assertQuery("conflicts:" + change1.getId().get(), change2); assertQuery("conflicts:" + change2.getId().get(), change1); @@ -2856,9 +2882,9 @@ @Test public void cherrypick() throws Exception { assume().that(getSchema().hasField(ChangeField.CHERRY_PICK_SPEC)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newCherryPickChange(repo, "foo", change1.currentPatchSetId())); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newCherryPickChange(repo, "foo", change1.currentPatchSetId())); assertQuery("is:cherrypick", change2); assertQuery("-is:cherrypick", change1); @@ -2867,14 +2893,14 @@ @Test public void merge() throws Exception { assume().that(getSchema().hasField(ChangeField.MERGE_SPEC)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create()); RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create()); RevCommit commit3 = repo.parseBody(repo.commit().parent(commit2).add("file1", "contents3").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); RevCommit mergeCommit = repo.branch("master") .commit() @@ -2883,7 +2909,7 @@ .parent(commit3) .insertChangeId() .create(); - Change mergeChange = insert(repo, newChangeForCommit(repo, mergeCommit)); + Change mergeChange = insert("repo", newChangeForCommit(repo, mergeCommit)); assertQuery("status:open is:merge", mergeChange); assertQuery("status:open -is:merge", change3, change2, change1); @@ -2893,10 +2919,10 @@ @Test public void reviewedBy() throws Exception { resetTimeWithClockStep(2, MINUTES); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); gApi.changes().id(change1.getId().get()).current().review(new ReviewInput().message("comment")); @@ -2907,7 +2933,7 @@ gApi.changes().id(change2.getId().get()).current().review(new ReviewInput().message("comment")); PatchSet.Id ps3_1 = change3.currentPatchSetId(); - change3 = newPatchSet(repo, change3, user, /* message= */ Optional.empty()); + change3 = newPatchSet("repo", change3, user, /* message= */ Optional.empty()); assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1); // Response to previous patch set still counts as reviewing. gApi.changes() @@ -2934,11 +2960,11 @@ @Test public void reviewerAndCc() throws Exception { Account.Id user1 = createAccount("user1"); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); - insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); + insert("repo", newChange(repo)); ReviewerInput rin = new ReviewerInput(); rin.reviewer = user1.toString(); @@ -2965,11 +2991,11 @@ @Test public void byReviewed() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id otherUser = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); assertQuery("is:reviewed"); assertQuery("status:reviewed"); @@ -2993,11 +3019,11 @@ accountManager.authenticate(authRequestFactory.createForUser("user2")).getAccountId(); Account.Id user3 = accountManager.authenticate(authRequestFactory.createForUser("user3")).getAccountId(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); ReviewerInput rin = new ReviewerInput(); rin.reviewer = user1.toString(); @@ -3037,7 +3063,7 @@ @Test public void reviewerAndCcByEmail() throws Exception { Project.NameKey project = Project.nameKey("repo"); - TestRepository<Repo> repo = createProject(project.get()); + repo = createAndOpenProject(project.get()); ConfigInput conf = new ConfigInput(); conf.enableReviewerByEmail = InheritableBoolean.TRUE; gApi.projects().name(project.get()).config(conf); @@ -3045,9 +3071,9 @@ String userByEmail = "un.registered@reviewer.com"; String userByEmailWithName = "John Doe <" + userByEmail + ">"; - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + insert("repo", newChange(repo)); ReviewerInput rin = new ReviewerInput(); rin.reviewer = userByEmailWithName; @@ -3070,16 +3096,16 @@ @Test public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception { Project.NameKey project = Project.nameKey("repo"); - TestRepository<Repo> repo = createProject(project.get()); + repo = createAndOpenProject(project.get()); ConfigInput conf = new ConfigInput(); conf.enableReviewerByEmail = InheritableBoolean.TRUE; gApi.projects().name(project.get()).config(conf); String userByEmail = "John Doe <un.registered@reviewer.com>"; - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - insert(repo, newChange(repo)); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + insert("repo", newChange(repo)); ReviewerInput rin = new ReviewerInput(); rin.reviewer = userByEmail; @@ -3098,9 +3124,9 @@ @Test public void submitRecords() throws Exception { Account.Id user1 = createAccount("user1"); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); gApi.changes().id(change1.getId().get()).current().review(ReviewInput.approve()); requestContext.setContext(newRequestContext(user1)); @@ -3111,7 +3137,7 @@ assertQuery("-is:submittable", change2); assertQuery("label:CodE-RevieW=ok", change1); - assertQuery("label:CodE-RevieW=ok,user=user", change1); + assertQuery("label:CodE-RevieW=ok,user=" + userAccount.preferredEmail(), change1); assertQuery("label:CodE-RevieW=ok,Administrators", change1); assertQuery("label:CodE-RevieW=ok,group=Administrators", change1); assertQuery("label:CodE-RevieW=ok,owner", change1); @@ -3130,10 +3156,10 @@ public void hasEdit() throws Exception { Account.Id user1 = createAccount("user1"); Account.Id user2 = createAccount("user2"); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); String changeId1 = change1.getKey().get(); - Change change2 = insert(repo, newChange(repo)); + Change change2 = insert("repo", newChange(repo)); String changeId2 = change2.getKey().get(); requestContext.setContext(newRequestContext(user1)); @@ -3154,10 +3180,10 @@ @Test public void byUnresolved() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - Change change3 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); + Change change3 = insert("repo", newChange(repo)); // Change1 has one resolved comment (unresolvedcount = 0) // Change2 has one unresolved comment (unresolvedcount = 1) @@ -3185,13 +3211,13 @@ @Test public void byCommitsOnBranchNotMerged() throws Exception { - TestRepository<Repo> tr = createProject("repo"); - testByCommitsOnBranchNotMerged(tr, ImmutableSet.of()); + createProject("repo"); + testByCommitsOnBranchNotMerged("repo", ImmutableSet.of()); } @Test public void byCommitsOnBranchNotMergedSkipsMissingChanges() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); ObjectId missing = repo.branch(PatchSet.id(Change.id(987654), 1).toRefName()) .commit() @@ -3199,41 +3225,45 @@ .insertChangeId() .create() .copy(); - testByCommitsOnBranchNotMerged(repo, ImmutableSet.of(missing)); + testByCommitsOnBranchNotMerged("repo", ImmutableSet.of(missing)); } - private void testByCommitsOnBranchNotMerged(TestRepository<Repo> repo, Collection<ObjectId> extra) + private void testByCommitsOnBranchNotMerged(String repo, Collection<ObjectId> extra) throws Exception { int n = 10; List<String> shas = new ArrayList<>(n + extra.size()); extra.forEach(i -> shas.add(i.name())); List<Integer> expectedIds = new ArrayList<>(n); BranchNameKey dest = null; - for (int i = 0; i < n; i++) { - ChangeInserter ins = newChange(repo); - insert(repo, ins); - if (dest == null) { - dest = ins.getChange().getDest(); + try (TestRepository<Repository> repository = + new TestRepository<>(repoManager.openRepository(Project.nameKey(repo)))) { + for (int i = 0; i < n; i++) { + ChangeInserter ins = newChange(repository); + insert("repo", ins); + if (dest == null) { + dest = ins.getChange().getDest(); + } + shas.add(ins.getCommitId().name()); + expectedIds.add(ins.getChange().getId().get()); } - shas.add(ins.getCommitId().name()); - expectedIds.add(ins.getChange().getId().get()); } - - for (int i = 1; i <= 11; i++) { - Iterable<ChangeData> cds = - queryProvider.get().byCommitsOnBranchNotMerged(repo.getRepository(), dest, shas, i); - Iterable<Integer> ids = FluentIterable.from(cds).transform(in -> in.getId().get()); - String name = "limit " + i; - assertWithMessage(name).that(ids).hasSize(n); - assertWithMessage(name).that(ids).containsExactlyElementsIn(expectedIds); + try (Repository repository = repoManager.openRepository(Project.nameKey(repo))) { + for (int i = 1; i <= 11; i++) { + Iterable<ChangeData> cds = + queryProvider.get().byCommitsOnBranchNotMerged(repository, dest, shas, i); + Iterable<Integer> ids = FluentIterable.from(cds).transform(in -> in.getId().get()); + String name = "limit " + i; + assertWithMessage(name).that(ids).hasSize(n); + assertWithMessage(name).that(ids).containsExactlyElementsIn(expectedIds); + } } } @Test public void reindexIfStale() throws Exception { Project.NameKey project = Project.nameKey("repo"); - TestRepository<Repo> repo = createProject(project.get()); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject(project.get()); + Change change = insert("repo", newChange(repo)); String changeId = change.getKey().get(); Account.Id anotherUser = createAccount("another-user"); @@ -3256,14 +3286,14 @@ @Test public void watched() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW); - Change change1 = insert(repo, ins1); + createProject("repo"); + ChangeInserter ins1 = newChangeWithStatus("repo", Change.Status.NEW); + Change change1 = insert("repo", ins1); - TestRepository<Repo> repo2 = createProject("repo2"); + createProject("repo2"); - ChangeInserter ins2 = newChangeWithStatus(repo2, Change.Status.NEW); - insert(repo2, ins2); + ChangeInserter ins2 = newChangeWithStatus("repo2", Change.Status.NEW); + insert("repo2", ins2); assertQuery("is:watched"); @@ -3283,17 +3313,17 @@ @Test public void trackingid() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("Change one\n\nBug:QUERY123").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("Change two\n\nIssue: Issue 16038\n").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); RevCommit commit3 = repo.parseBody(repo.commit().message("Change two\n\nGoogle-Bug-Id: b/16039\n").create()); - Change change3 = insert(repo, newChangeForCommit(repo, commit3)); + Change change3 = insert("repo", newChangeForCommit(repo, commit3)); assertQuery("tr:QUERY123", change1); assertQuery("bug:QUERY123", change1); @@ -3319,9 +3349,9 @@ @Test public void revertOf() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); // Create two commits and revert second commit (initial commit can't be reverted) - Change initial = insert(repo, newChange(repo)); + Change initial = insert("repo", newChange(repo)); gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(initial.getChangeId()).current().submit(); @@ -3336,10 +3366,10 @@ @Test public void submissionId() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); // create irrelevant change - insert(repo, newChange(repo)); + insert("repo", newChange(repo)); gApi.changes().id(change.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(change.getChangeId()).current().submit(); String submissionId = gApi.changes().id(change.getChangeId()).get().submissionId; @@ -3409,9 +3439,9 @@ return this; } - DashboardChangeState create(TestRepository<Repo> repo) throws Exception { + DashboardChangeState create(TestRepository<Repository> repo) throws Exception { requestContext.setContext(newRequestContext(ownerId)); - Change change = insert(repo, newChange(repo), ownerId); + Change change = insert("repo", newChange(repo), ownerId); id = change.getId(); ChangeApi cApi = gApi.changes().id(change.getChangeId()); if (assigneeId != null) { @@ -3479,7 +3509,7 @@ @Test public void dashboardHasUnpublishedDrafts() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id otherAccountId = createAccount("other"); DashboardChangeState hasUnpublishedDraft = new DashboardChangeState(otherAccountId).draftCommentBy(user.getAccountId()).create(repo); @@ -3497,7 +3527,7 @@ @Test public void dashboardAssignedReviews() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id otherAccountId = createAccount("other"); DashboardChangeState otherOpenWip = new DashboardChangeState(otherAccountId).wip().assignTo(user.getAccountId()).create(repo); @@ -3519,12 +3549,12 @@ // Viewing another user's dashboard. requestContext.setContext(newRequestContext(otherAccountId)); assertDashboardQuery( - user.getUserName().get(), IndexPreloadingUtil.DASHBOARD_ASSIGNED_QUERY, otherOpenWip); + userId.toString(), IndexPreloadingUtil.DASHBOARD_ASSIGNED_QUERY, otherOpenWip); } @Test public void dashboardWorkInProgressReviews() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); DashboardChangeState ownedOpenWip = new DashboardChangeState(user.getAccountId()).wip().create(repo); @@ -3539,7 +3569,7 @@ @Test public void dashboardOutgoingReviews() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id otherAccountId = createAccount("other"); DashboardChangeState ownedOpenReviewable = new DashboardChangeState(user.getAccountId()).create(repo); @@ -3554,14 +3584,12 @@ // Viewing another user's dashboard. requestContext.setContext(newRequestContext(otherAccountId)); assertDashboardQuery( - user.getUserName().get(), - IndexPreloadingUtil.DASHBOARD_OUTGOING_QUERY, - ownedOpenReviewable); + userId.toString(), IndexPreloadingUtil.DASHBOARD_OUTGOING_QUERY, ownedOpenReviewable); } @Test public void dashboardIncomingReviews() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id otherAccountId = createAccount("other"); DashboardChangeState reviewingReviewable = new DashboardChangeState(otherAccountId).addReviewer(user.getAccountId()).create(repo); @@ -3587,7 +3615,7 @@ // Viewing another user's dashboard. requestContext.setContext(newRequestContext(otherAccountId)); assertDashboardQuery( - user.getUserName().get(), + userId.toString(), IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY, assignedReviewable, reviewingReviewable); @@ -3595,7 +3623,7 @@ @Test public void dashboardRecentlyClosedReviews() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); Account.Id otherAccountId = createAccount("other"); DashboardChangeState mergedOwned = new DashboardChangeState(user.getAccountId()).mergeBy(user.getAccountId()).create(repo); @@ -3658,7 +3686,7 @@ // Viewing another user's dashboard. requestContext.setContext(newRequestContext(otherAccountId)); assertDashboardQuery( - user.getUserName().get(), + userId.toString(), IndexPreloadingUtil.DASHBOARD_RECENTLY_CLOSED_QUERY, abandonedAssignedWip, abandonedAssigned, @@ -3674,9 +3702,9 @@ public void attentionSetIndexed() throws Exception { assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue(); assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS_COUNT)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); AttentionSetInput input = new AttentionSetInput(userId.toString(), "some reason"); gApi.changes().id(change1.getChangeId()).addToAttentionSet(input); @@ -3685,7 +3713,7 @@ assertQuery("-is:attention", change2); assertQuery("has:attention", change1); assertQuery("-has:attention", change2); - assertQuery("attention:" + user.getUserName().get(), change1); + assertQuery("attention:" + userAccount.preferredEmail(), change1); assertQuery("-attention:" + userId.toString(), change2); gApi.changes() @@ -3698,8 +3726,8 @@ @Test public void attentionSetStored() throws Exception { assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); AttentionSetInput input = new AttentionSetInput(userId.toString(), "reason 1"); gApi.changes().id(change.getChangeId()).addToAttentionSet(input); @@ -3727,29 +3755,29 @@ @Test public void assignee() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChange(repo)); AssigneeInput input = new AssigneeInput(); - input.assignee = user.getUserName().get(); + input.assignee = user.getAccountId().toString(); gApi.changes().id(change1.getChangeId()).setAssignee(input); assertQuery("is:assigned", change1); assertQuery("-is:assigned", change2); assertQuery("is:unassigned", change2); assertQuery("-is:unassigned", change1); - assertQuery("assignee:" + user.getUserName().get(), change1); - assertQuery("-assignee:" + user.getUserName().get(), change2); + assertQuery("assignee:" + user.getAccountId(), change1); + assertQuery("-assignee:" + user.getAccountId(), change2); } @GerritConfig(name = "accounts.visibility", value = "NONE") @Test public void userDestination() throws Exception { - TestRepository<Repo> repo1 = createProject("repo1"); - Change change1 = insert(repo1, newChange(repo1)); - TestRepository<Repo> repo2 = createProject("repo2"); - Change change2 = insert(repo2, newChange(repo2)); + createProject("repo1"); + Change change1 = insert("repo1", newChange("repo1")); + createProject("repo2"); + Change change2 = insert("repo2", newChange("repo2")); assertThatQueryException("destination:foo") .hasMessageThat() @@ -3763,7 +3791,7 @@ String destination4 = "refs/heads/master\trepo3"; String destination5 = "refs/heads/other\trepo1"; - try (TestRepository<Repo> allUsers = + try (TestRepository<Repository> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName))) { String refsUsers = RefNames.refsUsers(userId); allUsers.branch(refsUsers).commit().add("destinations/destination1", destination1).create(); @@ -3814,26 +3842,25 @@ assertThatQueryException("destination:destination3,user=" + anotherUserId) .hasMessageThat() .isEqualTo("Unknown named destination: destination3"); - assertThatQueryException("destination:destination3,user=test") + assertThatQueryException("destination:destination3,user=non-existent") .hasMessageThat() - .isEqualTo("Account 'test' not found"); + .isEqualTo("Account 'non-existent' not found"); requestContext.setContext(newRequestContext(anotherUserId)); - // account 1000000 is not visible to 'anotheruser' as they are not an admin + // account userId is not visible to 'anotheruser' as they are not an admin assertThatQueryException("destination:destination3,user=" + userId) .hasMessageThat() - .isEqualTo("Account '1000000' not found"); + .isEqualTo(String.format("Account '%s' not found", userId)); } @GerritConfig(name = "accounts.visibility", value = "NONE") @Test public void userQuery() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChangeForBranch(repo, "stable")); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo)); + Change change2 = insert("repo", newChangeForBranch(repo, "stable")); - Account.Id anotherUserId = - accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); + Account.Id anotherUserId = createAccount("anotheruser"); String queryListText = "query1\tproject:repo\n" + "query2\tproject:repo status:open\n" @@ -3845,7 +3872,7 @@ + "query7\tproject:repo branch:stable\n" + "query8\tproject:repo branch:other"; - try (TestRepository<Repo> allUsers = + try (TestRepository<Repository> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName)); MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName); MetaDataUpdate anotherMd = metaDataUpdateFactory.create(allUsersName)) { @@ -3859,19 +3886,20 @@ anotherQueries.commit(anotherMd); } + assertThat(gApi.accounts().self().get()._accountId).isEqualTo(userId.get()); assertThatQueryException("query:foo").hasMessageThat().isEqualTo("Unknown named query: foo"); assertThatQueryException("query:query1,user=" + anotherUserId) .hasMessageThat() .isEqualTo("Unknown named query: query1"); - assertThatQueryException("query:query1,user=test") + assertThatQueryException("query:query1,user=non-existent") .hasMessageThat() - .isEqualTo("Account 'test' not found"); + .isEqualTo("Account 'non-existent' not found"); requestContext.setContext(newRequestContext(anotherUserId)); // account 1000000 is not visible to 'anotheruser' as they are not an admin assertThatQueryException("query:query1,user=" + userId) .hasMessageThat() - .isEqualTo("Account '1000000' not found"); + .isEqualTo(String.format("Account '%s' not found", userId)); requestContext.setContext(newRequestContext(userId)); assertQuery("query:query1", change2, change1); @@ -3890,16 +3918,16 @@ @Test public void byOwnerInvalidQuery() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + insert("repo", newChange(repo), userId); String nameEmail = user.asIdentifiedUser().getNameEmail(); assertQuery("owner: \"" + nameEmail + "\"\\"); } @Test public void byDeletedChange() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); String query = "change:" + change.getId(); assertQuery(query, change); @@ -3910,8 +3938,8 @@ @Test public void byUrlEncodedProject() throws Exception { - TestRepository<Repo> repo = createProject("repo+foo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo+foo"); + Change change = insert("repo+foo", newChange(repo)); assertQuery("project:repo+foo", change); } @@ -3944,9 +3972,9 @@ @Test public void isPureRevert() throws Exception { assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT_SPEC)).isTrue(); - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); // Create two commits and revert second commit (initial commit can't be reverted) - Change initial = insert(repo, newChange(repo)); + Change initial = insert("repo", newChange(repo)); gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(initial.getChangeId()).current().submit(); @@ -3990,8 +4018,8 @@ Account.Id user2 = accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId(); - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); gApi.changes().id(change.getId().get()).addReviewer(user2.toString()); RequestContext adminContext = requestContext.setContext(newRequestContext(user2)); @@ -4006,8 +4034,8 @@ @Test public void none() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change = insert(repo, newChange(repo)); + repo = createAndOpenProject("repo"); + Change change = insert("repo", newChange(repo)); assertQuery(ChangeIndexPredicate.none()); @@ -4027,27 +4055,24 @@ @Test @GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER") public void mergeableFailsWhenNotIndexed() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + assume().that(getSchema().hasField(ChangeField.MERGE_SPEC)).isTrue(); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create()); - insert(repo, newChangeForCommit(repo, commit1)); + insert("repo", newChangeForCommit(repo, commit1)); Throwable thrown = assertThrows(Throwable.class, () -> assertQuery("status:open is:mergeable")); 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 newChange(TestRepository<Repo> repo) throws Exception { - return newChange(repo, null, null, null, null, null, false, false); - } - - protected ChangeInserter newChangeForCommit(TestRepository<Repo> repo, RevCommit commit) + protected ChangeInserter newChangeForCommit(TestRepository<Repository> repo, RevCommit commit) throws Exception { return newChange(repo, commit, null, null, null, null, false, false); } - protected ChangeInserter newChangeWithFiles(TestRepository<Repo> repo, String... paths) + protected ChangeInserter newChangeWithFiles(TestRepository<Repository> repo, String... paths) throws Exception { TestRepository<?>.CommitBuilder b = repo.commit().message("Change with files"); for (String path : paths) { @@ -4056,36 +4081,67 @@ return newChangeForCommit(repo, repo.parseBody(b.create())); } - protected ChangeInserter newChangeForBranch(TestRepository<Repo> repo, String branch) + protected ChangeInserter newChangeForBranch(TestRepository<Repository> repo, String branch) throws Exception { return newChange(repo, null, branch, null, null, null, false, false); } - protected ChangeInserter newChangeWithStatus(TestRepository<Repo> repo, Change.Status status) - throws Exception { + protected ChangeInserter newChangeWithStatus( + TestRepository<Repository> repo, Change.Status status) throws Exception { return newChange(repo, null, null, status, null, null, false, false); } - protected ChangeInserter newChangeWithTopic(TestRepository<Repo> repo, String topic) + protected ChangeInserter newChangeWithStatus(String repoName, Change.Status status) + throws Exception { + return newChange(repoName, null, null, status, null, null, false, false); + } + + protected ChangeInserter newChangeWithTopic(TestRepository<Repository> repo, String topic) throws Exception { return newChange(repo, null, null, null, topic, null, false, false); } - protected ChangeInserter newChangeWorkInProgress(TestRepository<Repo> repo) throws Exception { + protected ChangeInserter newChangeWorkInProgress(TestRepository<Repository> repo) + throws Exception { return newChange(repo, null, null, null, null, null, true, false); } - protected ChangeInserter newChangePrivate(TestRepository<Repo> repo) throws Exception { + protected ChangeInserter newChangePrivate(TestRepository<Repository> repo) throws Exception { return newChange(repo, null, null, null, null, null, false, true); } protected ChangeInserter newCherryPickChange( - TestRepository<Repo> repo, String branch, PatchSet.Id cherryPickOf) throws Exception { + TestRepository<Repository> repo, String branch, PatchSet.Id cherryPickOf) throws Exception { return newChange(repo, null, branch, null, null, cherryPickOf, false, true); } + protected ChangeInserter newChange(String repoName) throws Exception { + return newChange(repoName, null, null, null, null, null, false, false); + } + protected ChangeInserter newChange( - TestRepository<Repo> repo, + String repoName, + @Nullable RevCommit commit, + @Nullable String branch, + @Nullable Change.Status status, + @Nullable String topic, + @Nullable PatchSet.Id cherryPickOf, + boolean workInProgress, + boolean isPrivate) + throws Exception { + try (TestRepository<Repository> repo = + new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) { + return newChange( + repo, commit, branch, status, topic, cherryPickOf, workInProgress, isPrivate); + } + } + + protected ChangeInserter newChange(TestRepository<Repository> repo) throws Exception { + return newChange(repo, null, null, null, null, null, false, false); + } + + protected ChangeInserter newChange( + TestRepository<Repository> repo, @Nullable RevCommit commit, @Nullable String branch, @Nullable Change.Status status, @@ -4095,7 +4151,7 @@ boolean isPrivate) throws Exception { if (commit == null) { - commit = repo.parseBody(repo.commit().message("message").create()); + commit = repo.parseBody(repo.commit().message("initial message").create()); } branch = MoreObjects.firstNonNull(branch, "refs/heads/master"); @@ -4104,32 +4160,32 @@ } Change.Id id = Change.id(seq.nextChangeId()); - ChangeInserter ins = - changeFactory - .create(id, commit, branch) - .setValidate(false) - .setStatus(status) - .setTopic(topic) - .setWorkInProgress(workInProgress) - .setPrivate(isPrivate) - .setCherryPickOf(cherryPickOf); - return ins; + return changeFactory + .create(id, commit, branch) + .setValidate(false) + .setStatus(status) + .setTopic(topic) + .setWorkInProgress(workInProgress) + .setPrivate(isPrivate) + .setCherryPickOf(cherryPickOf); } - protected Change insert(TestRepository<Repo> repo, ChangeInserter ins) throws Exception { - return insert(repo, ins, null, TimeUtil.now()); - } - - protected Change insert(TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner) + @CanIgnoreReturnValue + protected Change insert(String repoName, ChangeInserter ins, @Nullable Account.Id owner) throws Exception { - return insert(repo, ins, owner, TimeUtil.now()); + return insert(repoName, ins, owner, TimeUtil.now()); } + @CanIgnoreReturnValue + protected Change insert(String repoName, ChangeInserter ins) throws Exception { + return insert(repoName, ins, null, TimeUtil.now()); + } + + @CanIgnoreReturnValue protected Change insert( - TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn) + String repoName, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn) throws Exception { - Project.NameKey project = - Project.nameKey(repo.getRepository().getDescription().getRepositoryName()); + Project.NameKey project = Project.nameKey(repoName); Account.Id ownerId = owner != null ? owner : userId; IdentifiedUser user = userFactory.create(ownerId); try (BatchUpdate bu = updateFactory.create(project, user, createdOn)) { @@ -4140,34 +4196,36 @@ } protected Change newPatchSet( - TestRepository<Repo> repo, Change c, CurrentUser user, Optional<String> message) - throws Exception { - // Add a new file so the patch set is not a trivial rebase, to avoid default - // Code-Review label copying. - int n = c.currentPatchSetId().get() + 1; - RevCommit commit = - repo.parseBody( - repo.commit() - .message(message.orElse("message")) - .add("file" + n, "contents " + n) - .create()); + String repoName, Change c, CurrentUser user, Optional<String> message) throws Exception { + try (TestRepository<Repository> repo = + new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) { + // Add a new file so the patch set is not a trivial rebase, to avoid default + // Code-Review label copying. + int n = c.currentPatchSetId().get() + 1; + RevCommit commit = + repo.parseBody( + repo.commit() + .message(message.orElse("updated message")) + .add("file" + n, "contents " + n) + .create()); - PatchSetInserter inserter = - patchSetFactory - .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit) - .setFireRevisionCreated(false) - .setValidate(false); - try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now()); - ObjectInserter oi = repo.getRepository().newObjectInserter(); - ObjectReader reader = oi.newReader(); - RevWalk rw = new RevWalk(reader)) { - bu.setRepository(repo.getRepository(), rw, oi); - bu.setNotify(NotifyResolver.Result.none()); - bu.addOp(c.getId(), inserter); - bu.execute(); + PatchSetInserter inserter = + patchSetFactory + .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit) + .setFireRevisionCreated(false) + .setValidate(false); + try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now()); + ObjectInserter oi = repo.getRepository().newObjectInserter(); + ObjectReader reader = oi.newReader(); + RevWalk rw = new RevWalk(reader)) { + bu.setRepository(repo.getRepository(), rw, oi); + bu.setNotify(NotifyResolver.Result.none()); + bu.addOp(c.getId(), inserter); + bu.execute(); + } + + return inserter.getChange(); } - - return inserter.getChange(); } protected ThrowableSubject assertThatQueryException(Object query) throws Exception { @@ -4192,17 +4250,27 @@ } } - protected TestRepository<Repo> createProject(String name) throws Exception { - gApi.projects().create(name).get(); + @CanIgnoreReturnValue + protected TestRepository<Repository> createAndOpenProject(String name) throws Exception { + createProject(name); return new TestRepository<>(repoManager.openRepository(Project.nameKey(name))); } - protected TestRepository<Repo> createProject(String name, String parent) throws Exception { + protected TestRepository<Repository> createAndOpenProject(String name, String parent) + throws Exception { + createProject(name, parent); + return new TestRepository<>(repoManager.openRepository(Project.nameKey(name))); + } + + protected void createProject(String name) throws Exception { + gApi.projects().create(name).get(); + } + + protected void createProject(String name, String parent) throws Exception { ProjectInput input = new ProjectInput(); input.name = name; input.parent = parent; gApi.projects().create(input).get(); - return new TestRepository<>(repoManager.openRepository(Project.nameKey(name))); } protected QueryRequest newQuery(Object query) {
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java index fe60119..6b17bb6 100644 --- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java +++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -27,14 +27,13 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.index.change.ChangeIndexCollection; import com.google.gerrit.testing.InMemoryModule; -import com.google.gerrit.testing.InMemoryRepositoryManager; -import com.google.gerrit.testing.InMemoryRepositoryManager.Repo; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import java.util.List; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; import org.junit.Test; /** @@ -58,19 +57,21 @@ @UseClockStep public void stopQueryIfNoMoreResults() throws Exception { // create 2 visible changes - TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo"); - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); + try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) { + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + } // create 2 invisible changes - TestRepository<Repo> hiddenProject = createProject("hiddenProject"); - insert(hiddenProject, newChange(hiddenProject)); - insert(hiddenProject, newChange(hiddenProject)); - projectOperations - .project(Project.nameKey("hiddenProject")) - .forUpdate() - .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) - .update(); + try (TestRepository<Repository> hiddenProject = createAndOpenProject("hiddenProject")) { + insert("hiddenProject", newChange(hiddenProject)); + insert("hiddenProject", newChange(hiddenProject)); + projectOperations + .project(Project.nameKey("hiddenProject")) + .forUpdate() + .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) + .update(); + } AbstractFakeIndex<?, ?, ?> idx = (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex(); @@ -83,12 +84,13 @@ @Test @UseClockStep public void noLimitQueryPaginates() throws Exception { - TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo"); - // create 4 changes - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); + try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) { + // create 4 changes + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + } // Set queryLimit to 2 projectOperations @@ -111,11 +113,12 @@ @UseClockStep public void internalQueriesPaginate() throws Exception { // create 4 changes - TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo"); - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); - insert(testRepo, newChange(testRepo)); + try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) { + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + insert("repo", newChange(testRepo)); + } // Set queryLimit to 2 projectOperations
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java index 9717bfb..5cae012 100644 --- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java +++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -24,11 +24,9 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.testing.InMemoryModule; -import com.google.gerrit.testing.InMemoryRepositoryManager.Repo; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; -import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Test; @@ -45,11 +43,11 @@ @Test public void fullTextWithSpecialChars() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); RevCommit commit1 = repo.parseBody(repo.commit().message("foo_bar_foo").create()); - Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + Change change1 = insert("repo", newChangeForCommit(repo, commit1)); RevCommit commit2 = repo.parseBody(repo.commit().message("one.two.three").create()); - Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + Change change2 = insert("repo", newChangeForCommit(repo, commit2)); assertQuery("message:foo_ba"); assertQuery("message:bar", change1); @@ -63,8 +61,8 @@ @Test @Override public void byOwnerInvalidQuery() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + repo = createAndOpenProject("repo"); + Change change1 = insert("repo", newChange(repo), userId); String nameEmail = user.asIdentifiedUser().getNameEmail(); BadRequestException thrown = @@ -76,17 +74,17 @@ @Test public void openAndClosedChanges() throws Exception { - TestRepository<Repo> repo = createProject("repo"); + repo = createAndOpenProject("repo"); // create 3 closed changes - Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); - Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); - Change change3 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); + Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED)); + Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED)); + Change change3 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED)); // create 3 new changes - Change change4 = insert(repo, newChangeWithStatus(repo, Change.Status.NEW)); - Change change5 = insert(repo, newChangeWithStatus(repo, Change.Status.NEW)); - Change change6 = insert(repo, newChangeWithStatus(repo, Change.Status.NEW)); + Change change4 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW)); + Change change5 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW)); + Change change6 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW)); // Set queryLimit to 1 projectOperations
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..a9cbcbd 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,10 +1444,10 @@ bottomText: this.changeComments.computeCommentsString( this.patchRange, path, - this.files.changeFilesByPath[path], + file, /* includeUnmodified= */ true ), - file: {...this.files.changeFilesByPath[path], __path: path}, + file, }); } return dropdownContent; @@ -1893,45 +1455,24 @@ // 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, - }) - ); + 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..1999a82 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,39 @@ 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); // 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 +1876,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 +1906,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 +1971,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-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts index c8663e6..627ea27 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -198,7 +198,16 @@ text = htmlEscape(text).toString(); // Unescape block quotes '>'. This is slightly dangerous as '>' can be used // in HTML fragments, but it is insufficient on it's own. - text = text.replace(/(^|\n)>/g, '$1>'); + for (;;) { + const newText = text.replace( + /(^|\n)((?:\s{0,3}>)*\s{0,3})>/g, + '$1$2>' + ); + if (newText === text) { + break; + } + text = newText; + } return text; }
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts index 9acca0f..3881c62 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -593,5 +593,27 @@ ` ); }); + + test('renders nested block quotes', async () => { + element.content = '> > > block quote'; + await element.updateComplete; + + assert.shadowDom.equal( + element, + /* HTML */ ` + <marked-element> + <div slot="markdown-html" class="markdown-html"> + <blockquote> + <blockquote> + <blockquote> + <p>block quote</p> + </blockquote> + </blockquote> + </blockquote> + </div> + </marked-element> + ` + ); + }); }); });
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-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts index 1c09372..28e83ae 100644 --- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts +++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -16,7 +16,11 @@ DiffPreferencesInfo, } from '../../../api/diff'; import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group'; -import {countLines, diffClasses} from '../gr-diff/gr-diff-utils'; +import { + countLines, + diffClasses, + getResponsiveMode, +} from '../gr-diff/gr-diff-utils'; import {GrDiffRow} from './gr-diff-row'; import '../gr-context-controls/gr-context-controls-section'; import '../gr-context-controls/gr-context-controls'; @@ -71,6 +75,7 @@ if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly'); const pairs = this.getLinePairs(); + const responsiveMode = getResponsiveMode(this.diffPrefs, this.renderPrefs); const body = html` <tbody class=${diffClasses(...extras)}> ${this.renderContextControls()} ${this.renderMoveControls()} @@ -86,6 +91,7 @@ .lineLength=${this.diffPrefs?.line_length ?? 80} .tabSize=${this.diffPrefs?.tab_size ?? 2} .unifiedDiff=${this.isUnifiedDiff()} + .responsiveMode=${responsiveMode} > </gr-diff-row> `;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts index f7cc7e5..c1b13ac 100644 --- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts +++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
@@ -115,7 +115,7 @@ } const piece = html`<span class=${diffClasses('tab')} - style=${styleMap({'tab-size': `${this.tabSize}`})} + style=${styleMap({'tab-size': `${tabSize}`})} >${TAB}</span >`; this.pieces.push(piece);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts index 8f0503a..a0e7840 100644 --- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts +++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -75,18 +75,25 @@ test('tab wrapper style', async () => { element.lineLimit = 100; - element.text = '\t'; - await element.updateComplete; - for (const size of [1, 3, 8, 55]) { - element.tabSize = size; - await element.updateComplete; - await check( - '\t', - /* HTML */ ` - <span class="gr-diff tab" style="tab-size: ${size};"> </span> - ` - ); - } + element.tabSize = 4; + await check( + '\t', + /* HTML */ '<span class="gr-diff tab" style="tab-size:4;"></span>' + ); + await check( + 'abc\t', + /* HTML */ 'abc<span class="gr-diff tab" style="tab-size:1;"></span>' + ); + + element.tabSize = 8; + await check( + '\t', + /* HTML */ '<span class="gr-diff tab" style="tab-size:8;"></span>' + ); + await check( + 'abc\t', + /* HTML */ 'abc<span class="gr-diff tab" style="tab-size:5;"></span>' + ); }); test('tab wrapper insertion', async () => {
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 f583c2e..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,15 +48,21 @@ }, 0); } +export function isFileUnchanged(diff: DiffInfo) { + return !diff.content.some( + content => (content.a && !content.common) || (content.b && !content.common) + ); +} + export function getResponsiveMode( - prefs: DiffPreferencesInfo, + prefs?: DiffPreferencesInfo, renderPrefs?: RenderPreferences ): DiffResponsiveMode { if (renderPrefs?.responsive_mode) { return renderPrefs.responsive_mode; } // Backwards compatibility to the line_wrapping param. - if (prefs.line_wrapping) { + if (prefs?.line_wrapping) { return 'FULL_RESPONSIVE'; } return 'NONE';
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..0683af03 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/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