Merge branch 'stable-3.7'
* stable-3.7:
NoteDbUpdateManager: Fix wrong conflict resolution
LuceneChangeIndex: Remove commented out code
Set version to 3.7.1-SNAPSHOT
RBE: Update toolchain with bazel-toolchains 5.1.2 release
Set version to 3.7.0
Set version to 3.6.4-SNAPSHOT
Set version to 3.6.3
Set version to 3.5.5-SNAPSHOT
Set version to 3.5.4
CopyApprovalsIT: remove unnecessary comments
Log progress of the copy-approvals program
copy-approvals: do not lock loose refs when executing BatchRefUpdate
copy-approvals: multi-threaded, slice based
Replace "UTF-8" String with StandardCharsets.UTF_8
Update git submodules
Set version to 3.4.9-SNAPSHOT
Set version to 3.4.8
Fix bulk loading of entries with PassthroughLoadingCache
Update git submodules
Set version to 3.4.8-SNAPSHOT
Set version to 3.4.7
Introduce a PassthroughLoadingCache for disabled caches
Export commons:lang3 dependency to plugin API
copy-approvals: continue when there are corrupt meta-refs in a project
copy-approvals: don't stop when it fails on one project
Fix CopyApprovalsIT.multipleProjects: make sure to use the secondProject
Release-Notes: skip
Forward-Compatible: checked
Change-Id: Id9b470f7fddcae25f3b9017d0ae544915a29d228
diff --git a/.bazelversion b/.bazelversion
index 0062ac9..c7cb131 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-5.0.0
+5.3.1
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index e880314..d50e69d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2767,6 +2767,39 @@
when this parameter is removed and the system group uses the default
name again.
+[[groups.relevantGroup]]groups.relevantGroup::
++
+UUID of an external group that should always be considered as relevant
+when checking whether an account is visible.
++
+This setting is only relevant for external group backends and only if
+the link:#accounts.visibility[account visibility] is set to
+`SAME_GROUP` or `VISIBLE_GROUP`.
++
+If the link:#accounts.visibility[account visibility] is set to
+`SAME_GROUP` or `VISIBLE_GROUP` users should see all accounts that are
+a member of a group that contains themselves or that is visible to
+them. Checking this would require getting all groups of the current
+user and all groups of the accounts for which the visibility is being
+checked, but since getting all groups that a user is a member of is
+expensive for external group backends Gerrit doesn't query these groups
+but instead guesses the relevant groups. Guessing relevant groups
+limits the inspected groups to all groups that are mentioned in the
+ACLs of the projects that are currently cached (i.e. all groups that
+are listed in the link:config-project-config.html#file-groups[groups]
+files of the cached projects). This is not very reliable since it
+depends on which groups are mentioned in the ACLs and which projects
+are currently cached. To make this more reliable this configuration
+parameter allows to configure external groups that should always be
+considered as relevant.
++
+As said this setting is only relevant for external group backends. In
+Gerrit core this is only the LDAP backend, but it may apply to further
+group backends that are added by plugins.
++
+This parameter may be added multiple times to specify multiple relevant
+groups.
+
[[has-operand-alias]]
=== Section has operand alias
@@ -4297,6 +4330,14 @@
+
Default is 5 seconds. Negative values will be converted to 0.
+[[plugins.transitionalPushOptions]]plugins.transitionalPushOptions::
++
+Additional push options which should be accepted by gerrit as valid
+options even if they are not registered by any plugin(e.g. "myplugin~foo").
++
+This config can be used when gerrit migrates from a deprecated plugin to the new one. The new plugin
+can (temporary) accept push options of the old plugin without registering such options.
+
[[receive]]
=== Section receive
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index e4eee10..325353c 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -202,7 +202,7 @@
To remove a label in a child project, add an empty label with the same
name as in the parent. This will override the parent label with
-a label containing the defaults (`function = MaxWithBlock`,
+a label containing the defaults (`function = NoBlock`,
`defaultValue = 0` and no further allowed values)
[[label_layout]]
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 8298be3..601f2bf 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -187,6 +187,20 @@
redefine a submit requirement in a child project and make the submit requirement
always non-applicable.
+[[operator_has_submodule_update]]
+has:submodule-update::
++
+An operator that returns true if the diff of the latest patchset against the
+default parent has a submodule modified file, that is, a ".gitmodules" or a
+git link file.
++
+The optional `base` parameter can also be supplied for merge commits like
+`has:submodule-update,base=1`, or `has:submodule-update,base=2`. In these cases,
+the operator returns true if the diff of the latest patchset against parent
+number identified by `base` has a submodule modified file. Note that the
+operator will return false if the base parameter is greater than the number of
+parents for the latest patchset for the change.
+
[[operator_file]]
file:"'<filePattern>',withDiffContaining='<contentPattern>'"::
+
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index ca72f8b..33c5bbd 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -533,6 +533,24 @@
Certain operations in Gerrit can be validated by plugins by
implementing the corresponding link:config-validation.html[listeners].
+[[taskListeners]]
+== WorkQueue.TaskListeners
+
+It is possible for plugins to listen to
+`com.google.gerrit.server.git.WorkQueue$Task`s directly before they run, and
+directly after they complete. This may be used to delay task executions based
+on custom criteria by blocking, likely on a lock or semaphore, inside
+onStart(), and a lock/semaphore release in onStop(). Plugins may listen to
+tasks by implementing a `com.google.gerrit.server.git.WorkQueue$TaskListener`
+and registering the new listener like this:
+
+[source,java]
+----
+bind(TaskListener.class)
+ .annotatedWith(Exports.named("MyListener"))
+ .to(MyListener.class);
+----
+
[[change-message-modifier]]
== Change Message Modifier
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 9afd6e3..c3d79b1 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -15,6 +15,7 @@
== Contributor Guides
. link:dev-community.html[Gerrit Community]
. link:dev-community.html#how-to-contribute[How to Contribute]
+.. link:dev-readme.html[Developer Setup]
== User Guides
. link:intro-user.html[User Guide]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ab7fb3b..b5b3921 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2124,6 +2124,66 @@
HTTP/1.1 204 No Content
----
+[[apply-patch]]
+=== Create patch-set from patch
+--
+'POST /changes/link:#change-id[\{change-id\}]/patch:apply'
+--
+
+Creates a new patch set on a destination change from the provided patch.
+
+The patch must be provided in the request body, inside a
+link:#applypatchpatchset-input[ApplyPatchPatchSetInput] entity.
+
+If a base commit is given, the patch is applied on top of it. Otherwise, the
+patch is applied on top of the target change's branch tip.
+
+Applying the patch will fail if the destination change is closed, or in case of any conflicts.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/patch:apply HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "patch": {
+ "patch": "new file mode 100644\n--- /dev/null\n+++ b/a_new_file.txt\n@@ -0,0 +1,2 @@ \
++Patch compatible `git diff` output \
++For example: `link:#get-patch[<gerrit patch>] | base64 -d | sed -z 's/\n/\\n/g'`"
+ }
+ }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the destination change after applying the patch.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "project": "myProject",
+ "branch": "release-branch",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "subject": "Original change subject",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "insertions": 12,
+ "deletions": 11,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ },
+ "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c"
+ }
+----
+
[[get-included-in]]
=== Get Included In
--
@@ -2927,12 +2987,19 @@
Content-Type: application/json; charset=UTF-8
{
- "binary_content": "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="
+ "binary_content": "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==",
+ "file_mode": 100755
}
----
Note that it must be base-64 encoded data uri.
+The "file_mode" field is optional, and if provided must be in octal format. The field
+indicates whether the file is executable or not and has a value of either 100755
+(executable) or 100644 (not executable). If it's unset, this indicates no change
+has been made. New files default to not being executable if this parameter is not
+provided
+
When change edit doesn't exist for this change yet it is created. When file
content isn't provided, it is wiped out for that file. As response
"`204 No Content`" is returned.
@@ -6506,6 +6573,46 @@
at the server or permissions are modified. Not present if false.
|====================================
+[[applypatch-input]]
+=== ApplyPatchInput
+The `ApplyPatchInput` entity contains information about a patch to apply.
+
+A new commit will be created from the patch, and saved as a new patch set.
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name ||Description
+|`patch` |required|
+The patch to be applied. Must be compatible with `git diff` output.
+For example, link:#get-patch[Get Patch] output.
+|=================================
+
+[[applypatchpatchset-input]]
+=== ApplyPatchPatchSetInput
+The `ApplyPatchPatchSetInput` entity contains information for creating a new patch set from a
+given patch.
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name ||Description
+|`patch` |required|
+The details of the patch to be applied as a link:#applypatch-input[ApplyPatchInput] entity.
+|`commit_message` |optional|
+The commit message for the new patch set. If not specified, a predefined message will be used.
+|`base` |optional|
+40-hex digit SHA-1 of the commit which will be the parent commit of the newly created patch set.
+If set, it must be a merged commit or a change revision on the destination branch.
+Otherwise, the target change's branch tip will be used.
+|`author` |optional|
+The author of the commit to create. Must be an
+link:rest-api-accounts.html#account-input[AccountInput] entity with at least
+the `name` and `email` fields set.
+The caller needs "Forge Author" permission when using this field, unless specifies their own details.
+This field does not affect the owner of the change, which will continue to use the identity of the
+caller.
+|=================================
+
+
[[approval-info]]
=== ApprovalInfo
The `ApprovalInfo` entity contains information about an approval from a
@@ -6878,10 +6985,12 @@
If set, the target branch (see `branch` field) must exist (it is not
possible to create it automatically by setting the `new_branch` field
to `true`.
+|`patch` |optional|
+The detail of a patch to be applied as an link:#applypatch-input[ApplyPatchInput] entity.
|`author` |optional|
-An link:rest-api-accounts.html#account-input[AccountInput] entity
-that will set the author of the commit to create. The author must be
-specified as name/email combination.
+The author of the commit to create. Must be an
+link:rest-api-accounts.html#account-input[AccountInput] entity with at least
+the `name` and `email` fields set.
The caller needs "Forge Author" permission when using this field.
This field does not affect the owner of the change, which will
continue to use the identity of the caller.
@@ -7471,8 +7580,13 @@
differ by one from details provided in <<diff-info,DiffInfo>>.
|`size_delta` ||
Number of bytes by which the file size increased/decreased.
-|`size` ||
-File size in bytes.
+|`size` || File size in bytes.
+|`old_mode` |optional|File mode in octal (e.g. 100644) at the old commit.
+The first three digits indicate the file type and the last three digits contain
+the file permission bits. For added files, this field will not be present.
+|`new_mode` |optional|File mode in octal (e.g. 100644) at the new commit.
+The first three digits indicate the file type and the last three digits contain
+the file permission bits. For deleted files, this field will not be present.
|=============================
[[fix-input]]
@@ -7714,11 +7828,11 @@
The detail of the source commit for merge as a link:#merge-input[MergeInput]
entity.
|`author` |optional|
-An link:rest-api-accounts.html#account-input[AccountInput] entity
-that will set the author of the commit to create. The author must be
-specified as name/email combination.
+The author of the commit to create. Must be an
+link:rest-api-accounts.html#account-input[AccountInput] entity with at least
+the `name` and `email` fields set.
The caller needs "Forge Author" permission when using this field.
-This field does not affect the owner of the change, which will
+This field does not affect the owner or the committer of the change, which will
continue to use the identity of the caller.
|==================================
@@ -7965,7 +8079,9 @@
Topic can't contain quotation marks.
|`work_in_progress` |optional|
When present, change is marked as Work In Progress. The `notify` input is
-used if it's present, otherwise it will be overridden to `OWNER`. +
+used if it's present, otherwise it will be overridden to `NONE`. +
+Notifications for the reverted change will only sent once the result change is
+no longer WIP. +
If not set, the default is false.
|`validation_options`|optional|
Map with key-value pairs that are forwarded as options to the commit validation
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index efc746a..94db15e 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3804,13 +3804,13 @@
|`enabled` |optional|Whether the commentlink is enabled, as documented
in link:config-gerrit.html#commentlink.name.enabled[
commentlink.name.enabled]. If not set the commentlink is enabled.
+|==================================================
[[commentlink-input]]
=== CommentLinkInput
The `CommentLinkInput` entity describes the input for a
link:config-gerrit.html#commentlink[commentlink].
-|==================================================
[options="header",cols="1,^2,4"]
|==================================================
|Field Name | |Description
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 469bee5..348af76 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -76,6 +76,16 @@
headers. If the named resource already exists the server will respond
with HTTP 412 Precondition Failed.
+[[backwards-compatibility]]
+=== Backwards Compatibility
+
+The REST API is regularly extended (e.g. addition of new REST endpoints or new fields in existing
+JSON entities). Callers of the REST API must be able to deal with this (e.g. ignore unknown fields
+in the REST responses). Incompatible changes (e.g. removal of REST endpoints, altering/removal of
+existing fields in JSON entities) are avoided if possible, but can happen in rare cases. If they
+happen, they are announced in the link:https://www.gerritcodereview.com/releases-readme.html[release
+notes].
+
[[output]]
=== Output Format
JSON responses are encoded using UTF-8 and use content type
diff --git a/Documentation/user-search-accounts.txt b/Documentation/user-search-accounts.txt
index 6bcd18e..d5318c9 100644
--- a/Documentation/user-search-accounts.txt
+++ b/Documentation/user-search-accounts.txt
@@ -26,7 +26,10 @@
[[cansee]]
cansee:'CHANGE'::
+
-Matches accounts that can see the change 'CHANGE'.
+Matches accounts that can see the change 'CHANGE'. If the change is private,
+this operator will match with the owner/reviewers/ccs of the change if the
+caller is in owner/reviewers/ccs of the change. Otherwise, the request will fail
+with 404 `Bad Request` with "change not found" message.
[[email]]
email:'EMAIL'::
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index f260b87..9527a56 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -620,7 +620,7 @@
+
Changes containing a top-level or inline comment by 'USER'. The special
case of `commentby:self` will find changes where the caller has
-commented.
+commented. Note that setting a vote is also considered as a comment.
[[from]]
from:'USER'::
diff --git a/WORKSPACE b/WORKSPACE
index cf399dc..047da6a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -65,8 +65,8 @@
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "0fad45a9bda7dc1990c47b002fd64f55041ea751fafc00cd34efb96107675778",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.0/rules_nodejs-5.5.0.tar.gz"],
+ sha256 = "c29944ba9b0b430aadcaf3bf2570fece6fc5ebfb76df145c6cdad40d65c20811",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.7.0/rules_nodejs-5.7.0.tar.gz"],
)
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
@@ -136,8 +136,8 @@
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
node_repositories(
- node_version = "16.15.0",
- yarn_version = "1.22.18",
+ node_version = "17.9.1",
+ yarn_version = "1.22.19",
)
yarn_install(
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 1177734..3e48eec 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -32,6 +32,7 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
@@ -89,6 +90,7 @@
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -98,6 +100,7 @@
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.GerritPersonIdent;
@@ -1100,6 +1103,42 @@
gApi.changes().id(id).current().review(ReviewInput.recommend());
}
+ protected void assertThatAccountIsNotVisible(TestAccount... testAccounts) {
+ for (TestAccount testAccount : testAccounts) {
+ assertThrows(
+ ResourceNotFoundException.class, () -> gApi.accounts().id(testAccount.id().get()).get());
+ }
+ }
+
+ protected void assertReviewers(String changeId, TestAccount... expectedReviewers)
+ throws RestApiException {
+ Map<ReviewerState, Collection<AccountInfo>> reviewerMap =
+ gApi.changes().id(changeId).get().reviewers;
+ assertThat(reviewerMap).containsKey(ReviewerState.REVIEWER);
+ List<Integer> reviewers =
+ reviewerMap.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
+ assertThat(reviewers)
+ .containsExactlyElementsIn(
+ Arrays.stream(expectedReviewers).map(a -> a.id().get()).collect(toList()));
+ }
+
+ protected void assertCcs(String changeId, TestAccount... expectedCcs) throws RestApiException {
+ Map<ReviewerState, Collection<AccountInfo>> reviewerMap =
+ gApi.changes().id(changeId).get().reviewers;
+ assertThat(reviewerMap).containsKey(ReviewerState.CC);
+ List<Integer> ccs =
+ reviewerMap.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
+ assertThat(ccs)
+ .containsExactlyElementsIn(
+ Arrays.stream(expectedCcs).map(a -> a.id().get()).collect(toList()));
+ }
+
+ protected void assertNoCcs(String changeId) throws RestApiException {
+ Map<ReviewerState, Collection<AccountInfo>> reviewerMap =
+ gApi.changes().id(changeId).get().reviewers;
+ assertThat(reviewerMap).doesNotContainKey(ReviewerState.CC);
+ }
+
protected void assertSubmittedTogether(String chId, String... expected) throws Exception {
assertSubmittedTogether(chId, ImmutableSet.of(), expected);
}
@@ -1214,34 +1253,61 @@
assertThat(refValues.keySet()).containsAnyIn(trees.keySet());
}
- protected void assertDiffForNewFile(
- DiffInfo diff, RevCommit commit, String path, String expectedContentSideB) throws Exception {
- List<String> expectedLines = ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
+ protected void assertDiffForFullyModifiedFile(
+ DiffInfo diff,
+ String commitName,
+ String path,
+ String expectedContentSideA,
+ String expectedContentSideB)
+ throws Exception {
+ assertDiffForFile(diff, commitName, path);
- assertThat(diff.binary).isNull();
+ ImmutableList<String> expectedOldLines =
+ ImmutableList.copyOf(expectedContentSideA.split("\n", -1));
+ ImmutableList<String> expectedNewLines =
+ ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
+
+ assertThat(diff.changeType).isEqualTo(ChangeType.MODIFIED);
+
+ assertThat(diff.metaA).isNotNull();
+ assertThat(diff.metaB).isNotNull();
+
+ assertThat(diff.metaA.name).isEqualTo(path);
+ assertThat(diff.metaA.lines).isEqualTo(expectedOldLines.size());
+ assertThat(diff.metaB.name).isEqualTo(path);
+ assertThat(diff.metaB.lines).isEqualTo(expectedNewLines.size());
+
+ DiffInfo.ContentEntry contentEntry = diff.content.get(0);
+ assertThat(contentEntry.a).containsExactlyElementsIn(expectedOldLines).inOrder();
+ assertThat(contentEntry.b).containsExactlyElementsIn(expectedNewLines).inOrder();
+ assertThat(contentEntry.ab).isNull();
+ assertThat(contentEntry.common).isNull();
+ assertThat(contentEntry.editA).isNull();
+ assertThat(contentEntry.editB).isNull();
+ assertThat(contentEntry.skip).isNull();
+ }
+
+ protected void assertDiffForNewFile(
+ DiffInfo diff, @Nullable RevCommit commit, String path, String expectedContentSideB)
+ throws Exception {
+ assertDiffForNewFile(diff, commit.name(), path, expectedContentSideB);
+ }
+
+ protected void assertDiffForNewFile(
+ DiffInfo diff, String commitName, String path, String expectedContentSideB) throws Exception {
+ assertDiffForFile(diff, commitName, path);
+
+ ImmutableList<String> expectedLines =
+ ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
+
assertThat(diff.changeType).isEqualTo(ChangeType.ADDED);
- assertThat(diff.diffHeader).isNotNull();
- assertThat(diff.intralineStatus).isNull();
- assertThat(diff.webLinks).isNull();
- assertThat(diff.editWebLinks).isNull();
assertThat(diff.metaA).isNull();
assertThat(diff.metaB).isNotNull();
- assertThat(diff.metaB.commitId).isEqualTo(commit.name());
- String expectedContentType = "text/plain";
- if (COMMIT_MSG.equals(path)) {
- expectedContentType = FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE;
- } else if (MERGE_LIST.equals(path)) {
- expectedContentType = FileContentUtil.TEXT_X_GERRIT_MERGE_LIST;
- }
- assertThat(diff.metaB.contentType).isEqualTo(expectedContentType);
-
- assertThat(diff.metaB.lines).isEqualTo(expectedLines.size());
assertThat(diff.metaB.name).isEqualTo(path);
- assertThat(diff.metaB.webLinks).isNull();
+ assertThat(diff.metaB.lines).isEqualTo(expectedLines.size());
- assertThat(diff.content).hasSize(1);
DiffInfo.ContentEntry contentEntry = diff.content.get(0);
assertThat(contentEntry.b).containsExactlyElementsIn(expectedLines).inOrder();
assertThat(contentEntry.a).isNull();
@@ -1252,6 +1318,57 @@
assertThat(contentEntry.skip).isNull();
}
+ protected void assertDiffForDeletedFile(DiffInfo diff, String path, String expectedContentSideA)
+ throws Exception {
+ assertDiffHeaders(diff);
+
+ ImmutableList<String> expectedOriginalLines =
+ ImmutableList.copyOf(expectedContentSideA.split("\n", -1));
+
+ assertThat(diff.changeType).isEqualTo(ChangeType.DELETED);
+
+ assertThat(diff.metaA).isNotNull();
+ assertThat(diff.metaB).isNull();
+
+ assertThat(diff.metaA.name).isEqualTo(path);
+ assertThat(diff.metaA.lines).isEqualTo(expectedOriginalLines.size());
+
+ DiffInfo.ContentEntry contentEntry = diff.content.get(0);
+ assertThat(contentEntry.a).containsExactlyElementsIn(expectedOriginalLines).inOrder();
+ assertThat(contentEntry.b).isNull();
+ assertThat(contentEntry.ab).isNull();
+ assertThat(contentEntry.common).isNull();
+ assertThat(contentEntry.editA).isNull();
+ assertThat(contentEntry.editB).isNull();
+ assertThat(contentEntry.skip).isNull();
+ }
+
+ private void assertDiffForFile(DiffInfo diff, String commitName, String path) throws Exception {
+ assertDiffHeaders(diff);
+
+ assertThat(diff.metaB.commitId).isEqualTo(commitName);
+
+ String expectedContentType = "text/plain";
+ if (COMMIT_MSG.equals(path)) {
+ expectedContentType = FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE;
+ } else if (MERGE_LIST.equals(path)) {
+ expectedContentType = FileContentUtil.TEXT_X_GERRIT_MERGE_LIST;
+ }
+
+ assertThat(diff.metaB.contentType).isEqualTo(expectedContentType);
+
+ assertThat(diff.metaB.name).isEqualTo(path);
+ assertThat(diff.metaB.webLinks).isNull();
+ }
+
+ private void assertDiffHeaders(DiffInfo diff) throws Exception {
+ assertThat(diff.binary).isNull();
+ assertThat(diff.diffHeader).isNotNull();
+ assertThat(diff.intralineStatus).isNull();
+ assertThat(diff.webLinks).isNull();
+ assertThat(diff.editWebLinks).isNull();
+ }
+
protected void assertPermitted(ChangeInfo info, String label, Integer... expected) {
assertThat(info.permittedLabels).isNotNull();
Collection<String> strs = info.permittedLabels.get(label);
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index 91fbf9e..fe845c0 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -303,6 +303,7 @@
return pluginInfoFromChangeInfo(changeInfos.get(0));
}
+ @Nullable
protected static List<PluginDefinedInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
if (pluginInfo == null) {
@@ -331,6 +332,7 @@
* @param plugins list of {@code MyInfo} objects, each as a raw map returned from Gson.
* @return decoded list of {@code MyInfo}s.
*/
+ @Nullable
protected static List<PluginDefinedInfo> decodeRawPluginsList(
Gson gson, @Nullable Object plugins) {
if (plugins == null) {
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 4298663..9d237af 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -74,6 +74,7 @@
"//java/com/google/gerrit/gpg/testing:gpg-test-util",
"//java/com/google/gerrit/git/testing",
"//java/com/google/gerrit/index/testing",
+ "//lib/errorprone:annotations",
]
PGM_DEPLOY_ENV = [
diff --git a/java/com/google/gerrit/acceptance/HttpResponse.java b/java/com/google/gerrit/acceptance/HttpResponse.java
index 88079a4..76c0f04 100644
--- a/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
@@ -59,6 +60,7 @@
return getHeader("X-FYI-Content-Type");
}
+ @Nullable
public String getHeader(String name) {
Header hdr = response.getFirstHeader(name);
return hdr != null ? hdr.getValue() : null;
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 99db40a..5b1fa9b 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -24,6 +24,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.UsedAt.Project;
@@ -280,6 +281,12 @@
return this;
}
+ @CanIgnoreReturnValue
+ public PushOneCommit setTopLevelTreeId(ObjectId treeId) throws Exception {
+ commitBuilder.setTopLevelTree(treeId);
+ return this;
+ }
+
public PushOneCommit setParent(RevCommit parent) throws Exception {
commitBuilder.noParents();
commitBuilder.parent(parent);
@@ -291,6 +298,19 @@
return this;
}
+ public PushOneCommit addFile(String path, String content, int fileMode) throws Exception {
+ RevBlob blobId = testRepo.blob(content);
+ commitBuilder.edit(
+ new PathEdit(path) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.fromBits(fileMode));
+ ent.setObjectId(blobId);
+ }
+ });
+ return this;
+ }
+
public PushOneCommit addSymlink(String path, String target) throws Exception {
RevBlob blobId = testRepo.blob(target);
commitBuilder.edit(
diff --git a/java/com/google/gerrit/acceptance/config/BUILD b/java/com/google/gerrit/acceptance/config/BUILD
index a8ccc1f..0da68b0 100644
--- a/java/com/google/gerrit/acceptance/config/BUILD
+++ b/java/com/google/gerrit/acceptance/config/BUILD
@@ -7,6 +7,7 @@
srcs = glob(["*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//lib:guava",
"//lib:jgit",
"//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
index 24a2117..27ce857 100644
--- a/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
+++ b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
@@ -18,6 +18,7 @@
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -27,6 +28,7 @@
public class ConfigAnnotationParser {
private static Splitter splitter = Splitter.on(".").trimResults();
+ @Nullable
public static Config parse(Config base, GerritConfigs annotation) {
if (annotation == null) {
return null;
@@ -45,6 +47,7 @@
return cfg;
}
+ @Nullable
public static Map<String, Config> parse(GlobalPluginConfigs annotation) {
if (annotation == null || annotation.value().length < 1) {
return null;
@@ -67,6 +70,7 @@
return result;
}
+ @Nullable
public static Map<String, Config> parse(GlobalPluginConfig annotation) {
if (annotation == null) {
return null;
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
index d0ccd5b..1971c57 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
@@ -28,13 +28,18 @@
public class FileContentBuilder<T> {
private final T builder;
private final String filePath;
+ private final int newGitFileMode;
private final Consumer<TreeModification> modificationToBuilderAdder;
FileContentBuilder(
- T builder, String filePath, Consumer<TreeModification> modificationToBuilderAdder) {
+ T builder,
+ String filePath,
+ int newGitFileMode,
+ Consumer<TreeModification> modificationToBuilderAdder) {
checkNotNull(Strings.emptyToNull(filePath), "File path must not be null or empty.");
this.builder = builder;
this.filePath = filePath;
+ this.newGitFileMode = newGitFileMode;
this.modificationToBuilderAdder = modificationToBuilderAdder;
}
@@ -44,7 +49,7 @@
Strings.emptyToNull(content),
"Empty file content is not supported. Adjust test API if necessary.");
modificationToBuilderAdder.accept(
- new ChangeFileContentModification(filePath, RawInputUtil.create(content)));
+ new ChangeFileContentModification(filePath, RawInputUtil.create(content), newGitFileMode));
return builder;
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
index a064d02..f01a138 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -89,7 +89,18 @@
/** Modified file of the change. The file content is specified via the returned builder. */
public FileContentBuilder<Builder> file(String filePath) {
- return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
+ return new FileContentBuilder<>(this, filePath, 0, treeModificationsBuilder()::add);
+ }
+
+ /**
+ * Modified file of the change. The file content is specified via the returned builder. The
+ * second parameter indicates the git file mode for the modified file if it has been changed.
+ *
+ * @see org.eclipse.jgit.lib.FileMode
+ */
+ public FileContentBuilder<Builder> file(String filePath, int newGitFileMode) {
+ return new FileContentBuilder<>(
+ this, filePath, newGitFileMode, treeModificationsBuilder()::add);
}
abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
index fe9d909..22a4da6 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
@@ -45,7 +45,13 @@
/** Modified file of the patchset. The file content is specified via the returned builder. */
public FileContentBuilder<Builder> file(String filePath) {
- return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
+ return new FileContentBuilder<>(this, filePath, 0, treeModificationsBuilder()::add);
+ }
+
+ /** Modified file of the patchset. The file content is specified via the returned builder. */
+ public FileContentBuilder<Builder> file(String filePath, int newGitFileMode) {
+ return new FileContentBuilder<>(
+ this, filePath, newGitFileMode, treeModificationsBuilder()::add);
}
abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
index 850a133..d34b79a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -8,6 +8,7 @@
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/acceptance:function",
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index deeb843..b1cd506 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -26,6 +26,7 @@
import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupReference;
@@ -213,6 +214,7 @@
as -> as.upsertPermission(key.name()).setExclusiveGroup(exclusive)));
}
+ @Nullable
private RevCommit headOrNull(String branch) {
branch = RefNames.fullName(branch);
diff --git a/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java b/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java
index 8fb4d35..c40baba 100644
--- a/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java
+++ b/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java
@@ -39,6 +39,7 @@
return uuid.get().startsWith(LDAP_UUID);
}
+ @Nullable
@Override
public GroupDescription.Basic get(AccountGroup.UUID uuid) {
if (!handles(uuid)) {
diff --git a/java/com/google/gerrit/auth/ldap/Helper.java b/java/com/google/gerrit/auth/ldap/Helper.java
index a939c72..c11d045 100644
--- a/java/com/google/gerrit/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/auth/ldap/Helper.java
@@ -18,6 +18,7 @@
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.metrics.Description;
@@ -224,6 +225,7 @@
return ctx;
}
+ @Nullable
private DirContext kerberosOpen(Properties env)
throws IOException, LoginException, NamingException {
LoginContext ctx = new LoginContext("KerberosLogin");
diff --git a/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java b/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
index c3870f4..bb6480a 100644
--- a/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
+++ b/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
@@ -117,6 +117,7 @@
return isLdapUUID(uuid);
}
+ @Nullable
@Override
public GroupDescription.Basic get(AccountGroup.UUID uuid) {
if (!handles(uuid)) {
diff --git a/java/com/google/gerrit/auth/ldap/LdapQuery.java b/java/com/google/gerrit/auth/ldap/LdapQuery.java
index 409c9f5..71dc141 100644
--- a/java/com/google/gerrit/auth/ldap/LdapQuery.java
+++ b/java/com/google/gerrit/auth/ldap/LdapQuery.java
@@ -14,6 +14,7 @@
package com.google.gerrit.auth.ldap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.metrics.Timer0;
import java.util.ArrayList;
@@ -114,6 +115,7 @@
return get("dn");
}
+ @Nullable
String get(String attName) throws NamingException {
final Attribute att = getAll(attName);
return att != null && 0 < att.size() ? String.valueOf(att.get(0)) : null;
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index 7699799..7dc2b1b 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -20,6 +20,7 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -162,6 +163,7 @@
return vlist;
}
+ @Nullable
static String optdef(Config c, String n, String d) {
final String[] v = c.getStringList("ldap", null, n);
if (v == null || v.length == 0) {
@@ -184,6 +186,7 @@
return v;
}
+ @Nullable
static ParameterizedString paramString(Config c, String n, String d) {
String expression = optdef(c, n, d);
if (expression == null) {
@@ -209,6 +212,7 @@
return !readOnlyAccountFields.contains(field);
}
+ @Nullable
static String apply(ParameterizedString p, LdapQuery.Result m) throws NamingException {
if (p == null) {
return null;
@@ -306,6 +310,7 @@
usernameCache.put(who.getLocalUser(), Optional.of(account.id()));
}
+ @Nullable
@Override
public Account.Id lookup(String accountName) {
if (Strings.isNullOrEmpty(accountName)) {
diff --git a/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java
index b0c1f51..ab53cde 100644
--- a/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java
@@ -20,6 +20,7 @@
import com.google.common.base.Converter;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
@@ -109,6 +110,7 @@
this.encrypter = encrypter;
}
+ @Nullable
public OAuthToken get(Account.Id id) {
OAuthToken accessToken = cache.getIfPresent(id);
if (accessToken == null) {
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index 253266d..0a42d09 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.PermissionRange;
import java.util.ArrayList;
@@ -190,6 +191,7 @@
}
/** Returns the valid range for the capability if it has one, otherwise null. */
+ @Nullable
public static PermissionRange.WithDefaults getRange(String varName) {
if (QUERY_LIMIT.equalsIgnoreCase(varName)) {
return new PermissionRange.WithDefaults(
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 85dbdeb..699acc0 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -62,6 +62,7 @@
return Optional.ofNullable(Ints.tryParse(str)).map(Account::id);
}
+ @Nullable
public static Id fromRef(String name) {
if (name == null) {
return null;
@@ -82,11 +83,13 @@
* @param name a ref name with the following syntax: {@code "34/1234..."}. We assume that the
* caller has trimmed any prefix.
*/
+ @Nullable
public static Id fromRefPart(String name) {
Integer id = RefNames.parseShardedRefPart(name);
return id != null ? Account.id(id) : null;
}
+ @Nullable
public static Id parseAfterShardedRefPart(String name) {
Integer id = RefNames.parseAfterShardedRefPart(name);
return id != null ? Account.id(id) : null;
@@ -102,6 +105,7 @@
* @param name ref name
* @return account ID, or null if not numeric.
*/
+ @Nullable
public static Id fromRefSuffix(String name) {
Integer id = RefNames.parseRefSuffix(name);
return id != null ? Account.id(id) : null;
diff --git a/java/com/google/gerrit/entities/AccountGroup.java b/java/com/google/gerrit/entities/AccountGroup.java
index 001a544..b5c97da 100644
--- a/java/com/google/gerrit/entities/AccountGroup.java
+++ b/java/com/google/gerrit/entities/AccountGroup.java
@@ -15,6 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
public final class AccountGroup {
public static NameKey nameKey(String n) {
@@ -65,6 +66,7 @@
}
/** Parse an {@link AccountGroup.UUID} out of a ref-name. */
+ @Nullable
public static UUID fromRef(String ref) {
if (ref == null) {
return null;
@@ -81,6 +83,7 @@
* @param refPart a ref name with the following syntax: {@code "12/1234..."}. We assume that the
* caller has trimmed any prefix.
*/
+ @Nullable
public static UUID fromRefPart(String refPart) {
String uuid = RefNames.parseShardedUuidFromRefPart(refPart);
return uuid != null ? AccountGroup.uuid(uuid) : null;
diff --git a/java/com/google/gerrit/entities/Address.java b/java/com/google/gerrit/entities/Address.java
index 5d63476..eb1da46 100644
--- a/java/com/google/gerrit/entities/Address.java
+++ b/java/com/google/gerrit/entities/Address.java
@@ -46,6 +46,7 @@
throw new IllegalArgumentException("Invalid email address: " + in);
}
+ @Nullable
public static Address tryParse(String in) {
try {
return parse(in);
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index 66e1a96..55220f3 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -117,6 +117,7 @@
return id != null ? Optional.of(Change.id(id)) : Optional.empty();
}
+ @Nullable
public static Id fromRef(String ref) {
if (RefNames.isRefsEdit(ref)) {
return fromEditRefPart(ref);
@@ -134,6 +135,7 @@
return null;
}
+ @Nullable
public static Id fromAllUsersRef(String ref) {
if (ref == null) {
return null;
@@ -169,6 +171,7 @@
return true;
}
+ @Nullable
public static Id fromEditRefPart(String ref) {
int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) + RefNames.EDIT_PREFIX.length();
int endChangeId = nextNonDigit(ref, startChangeId);
@@ -179,6 +182,7 @@
return null;
}
+ @Nullable
public static Id fromRefPart(String ref) {
Integer id = RefNames.parseShardedRefPart(ref);
return id != null ? Change.id(id) : null;
@@ -404,6 +408,7 @@
return changeStatus;
}
+ @Nullable
public static Status forCode(char c) {
for (Status s : Status.values()) {
if (s.code == c) {
@@ -414,6 +419,7 @@
return null;
}
+ @Nullable
public static Status forChangeStatus(ChangeStatus cs) {
for (Status s : Status.values()) {
if (s.changeStatus == cs) {
@@ -599,6 +605,7 @@
}
/** Get the id of the most current {@link PatchSet} in this change. */
+ @Nullable
public PatchSet.Id currentPatchSetId() {
if (currentPatchSetId > 0) {
return PatchSet.id(changeId, currentPatchSetId);
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index 65a1559..e1e143c 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -49,6 +49,7 @@
return code;
}
+ @Nullable
public static Status forCode(char c) {
for (Status s : Status.values()) {
if (s.code == c) {
@@ -263,7 +264,7 @@
public void setLineNbrAndRange(
Integer lineNbr, com.google.gerrit.extensions.client.Comment.Range range) {
- this.lineNbr = lineNbr != null ? lineNbr : range != null ? range.endLine : 0;
+ this.lineNbr = range != null ? range.endLine : lineNbr != null ? lineNbr : 0;
if (range != null) {
this.range = new Comment.Range(range);
}
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index e31c764..f009872 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -132,6 +132,7 @@
return psa.labelId().get().equalsIgnoreCase(getName());
}
+ @Nullable
public LabelValue getMin() {
if (getValues().isEmpty()) {
return null;
@@ -139,6 +140,7 @@
return getValues().get(0);
}
+ @Nullable
public LabelValue getMax() {
if (getValues().isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index 2d28046..bef6580 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -19,6 +19,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import java.util.List;
@@ -112,6 +113,7 @@
}
@UsedAt(UsedAt.Project.COLLABNET)
+ @Nullable
public static ChangeType forCode(char c) {
for (ChangeType s : ChangeType.values()) {
if (s.code == c) {
@@ -168,33 +170,40 @@
*/
public enum FileMode implements CodedEnum {
/** Mode indicating an entry is a tree (aka directory). */
- TREE('T'),
+ TREE('T', 0040000),
/** Mode indicating an entry is a symbolic link. */
- SYMLINK('S'),
+ SYMLINK('S', 0120000),
/** Mode indicating an entry is a non-executable file. */
- REGULAR_FILE('R'),
+ REGULAR_FILE('R', 0100644),
/** Mode indicating an entry is an executable file. */
- EXECUTABLE_FILE('E'),
+ EXECUTABLE_FILE('E', 0100755),
/** Mode indicating an entry is a submodule commit in another repository. */
- GITLINK('G'),
+ GITLINK('G', 0160000),
/** Mode indicating an entry is missing during parallel walks. */
- MISSING('M');
+ MISSING('M', 0000000);
private final char code;
- FileMode(char c) {
+ private final int mode;
+
+ FileMode(char c, int m) {
code = c;
+ mode = m;
}
@Override
public char getCode() {
return code;
}
+
+ public int getMode() {
+ return mode;
+ }
}
private Patch() {}
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index 6c52368..354981c 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.errorprone.annotations.InlineMe;
+import com.google.gerrit.common.Nullable;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@@ -66,7 +67,7 @@
}
@AutoValue
- public abstract static class Id {
+ public abstract static class Id implements Comparable<Id> {
/** Parse a PatchSet.Id out of a string representation. */
public static Id parse(String str) {
List<String> parts = Splitter.on(',').splitToList(str);
@@ -83,6 +84,7 @@
}
/** Parse a PatchSet.Id from a {@link #refName()} result. */
+ @Nullable
public static Id fromRef(String ref) {
int cs = Change.Id.startIndex(ref);
if (cs < 0) {
@@ -145,6 +147,11 @@
public final String toString() {
return getCommaSeparatedChangeAndPatchSetId();
}
+
+ @Override
+ public int compareTo(Id other) {
+ return Ints.compare(get(), other.get());
+ }
}
public static Builder builder() {
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 95164bd..e3e3a57 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -124,6 +124,7 @@
return LABEL_AS + labelName;
}
+ @Nullable
public static String extractLabel(String varName) {
if (isLabel(varName)) {
return varName.substring(LABEL.length());
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 617b827..b587b1d 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -166,6 +166,7 @@
* @return name key of the parent project, {@code null} if this project is the All-Projects
* project
*/
+ @Nullable
public Project.NameKey getParent(Project.NameKey allProjectsName) {
if (getParent() != null) {
return getParent();
@@ -178,6 +179,7 @@
return allProjectsName;
}
+ @Nullable
public String getParentName() {
return getParent() != null ? getParent().get() : null;
}
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index b9c1b3c..9745fc5 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -16,6 +16,7 @@
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import java.util.List;
@@ -222,6 +223,7 @@
return REFS_CACHE_AUTOMERGE + hash.substring(0, 2) + '/' + hash.substring(2);
}
+ @Nullable
public static String shard(int id) {
if (id < 0) {
return null;
@@ -343,6 +345,7 @@
return GERRIT_REFS.stream().anyMatch(internalRef -> ref.startsWith(internalRef));
}
+ @Nullable
static Integer parseShardedRefPart(String name) {
if (name == null) {
return null;
@@ -386,6 +389,7 @@
}
@UsedAt(UsedAt.Project.PLUGINS_ALL)
+ @Nullable
public static String parseShardedUuidFromRefPart(String name) {
if (name == null) {
return null;
@@ -420,6 +424,7 @@
* @return the rest of the name, {@code null} if the ref name part doesn't start with a valid
* sharded ID
*/
+ @Nullable
static String skipShardedRefPart(String name) {
if (name == null) {
return null;
@@ -473,6 +478,7 @@
* ref name part doesn't start with a valid sharded ID or if no valid ID follows the sharded
* ref part
*/
+ @Nullable
static Integer parseAfterShardedRefPart(String name) {
String rest = skipShardedRefPart(name);
if (rest == null || !rest.startsWith("/")) {
@@ -493,6 +499,7 @@
return Integer.parseInt(rest.substring(0, ie));
}
+ @Nullable
public static Integer parseRefSuffix(String name) {
if (name == null) {
return null;
diff --git a/java/com/google/gerrit/extensions/api/changes/ApplyPatchInput.java b/java/com/google/gerrit/extensions/api/changes/ApplyPatchInput.java
new file mode 100644
index 0000000..493329c
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/ApplyPatchInput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2022 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.extensions.api.changes;
+
+/** Information about a patch to apply. */
+public class ApplyPatchInput {
+ /**
+ * Required. The patch to be applied.
+ *
+ * <p>Must be compatible with `git diff` output. For example, Gerrit API `Get Patch` output.
+ */
+ public String patch;
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/ApplyPatchPatchSetInput.java b/java/com/google/gerrit/extensions/api/changes/ApplyPatchPatchSetInput.java
new file mode 100644
index 0000000..872ea42
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/ApplyPatchPatchSetInput.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2022 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.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+
+/** Information for creating a new patch set from a given patch. */
+public class ApplyPatchPatchSetInput {
+
+ /** The patch to be applied. */
+ public ApplyPatchInput patch;
+
+ /**
+ * The commit message for the new patch set. If not specified, a predefined message will be used.
+ */
+ @Nullable public String commitMessage;
+
+ /**
+ * 40-hex digit SHA-1 of the commit which will be the parent commit of the newly created patch
+ * set. If set, it must be a merged commit or a change revision on the destination branch.
+ * Otherwise, the target change's branch tip will be used.
+ */
+ @Nullable public String base;
+
+ /**
+ * The author of the new patch set. Must include both {@link AccountInput#name} and {@link
+ * AccountInput#email} fields.
+ */
+ @Nullable public AccountInput author;
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 018a6cf..cce28e9 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -152,6 +152,8 @@
/** Create a merge patch set for the change. */
ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
+ ChangeInfo applyPatch(ApplyPatchPatchSetInput in) throws RestApiException;
+
default List<ChangeInfo> submittedTogether() throws RestApiException {
SubmittedTogetherInfo info =
submittedTogether(
@@ -824,6 +826,11 @@
}
@Override
+ public ChangeInfo applyPatch(ApplyPatchPatchSetInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public PureRevertInfo pureRevert() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
index 0cfe908..6349595 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
@@ -21,4 +21,5 @@
public class FileContentInput {
@DefaultInput public RawInput content;
public String binary_content;
+ public Integer fileMode;
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 11999ab..8bfe468 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -14,8 +14,10 @@
package com.google.gerrit.extensions.api.changes;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.FixSuggestionInfo;
@@ -117,11 +119,13 @@
public List<FixSuggestionInfo> fixSuggestions;
}
+ @CanIgnoreReturnValue
public ReviewInput message(String msg) {
message = msg != null && !msg.isEmpty() ? msg : null;
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput patchSetLevelComment(String message) {
Objects.requireNonNull(message);
CommentInput comment = new CommentInput();
@@ -131,6 +135,7 @@
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput label(String name, short value) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException();
@@ -142,6 +147,7 @@
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput label(String name, int value) {
if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) {
throw new IllegalArgumentException();
@@ -149,14 +155,22 @@
return label(name, (short) value);
}
+ @CanIgnoreReturnValue
public ReviewInput label(String name) {
return label(name, (short) 1);
}
+ @CanIgnoreReturnValue
public ReviewInput reviewer(String reviewer) {
- return reviewer(reviewer, REVIEWER, false);
+ return reviewer(reviewer, REVIEWER, /* confirmed= */ false);
}
+ @CanIgnoreReturnValue
+ public ReviewInput cc(String cc) {
+ return reviewer(cc, CC, /* confirmed= */ false);
+ }
+
+ @CanIgnoreReturnValue
public ReviewInput reviewer(String reviewer, ReviewerState state, boolean confirmed) {
ReviewerInput input = new ReviewerInput();
input.reviewer = reviewer;
@@ -169,6 +183,7 @@
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput addUserToAttentionSet(String user, String reason) {
AttentionSetInput input = new AttentionSetInput();
input.user = user;
@@ -180,6 +195,7 @@
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput removeUserFromAttentionSet(String user, String reason) {
AttentionSetInput input = new AttentionSetInput();
input.user = user;
@@ -191,17 +207,20 @@
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput blockAutomaticAttentionSetRules() {
ignoreAutomaticAttentionSetRules = true;
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput setWorkInProgress(boolean workInProgress) {
this.workInProgress = workInProgress;
ready = !workInProgress;
return this;
}
+ @CanIgnoreReturnValue
public ReviewInput setReady(boolean ready) {
this.ready = ready;
workInProgress = !ready;
diff --git a/java/com/google/gerrit/extensions/client/Side.java b/java/com/google/gerrit/extensions/client/Side.java
index e077df2..a87b37a 100644
--- a/java/com/google/gerrit/extensions/client/Side.java
+++ b/java/com/google/gerrit/extensions/client/Side.java
@@ -14,10 +14,13 @@
package com.google.gerrit.extensions.client;
+import com.google.gerrit.common.Nullable;
+
public enum Side {
PARENT,
REVISION;
+ @Nullable
public static Side fromShort(short s) {
if (s <= 0) {
return PARENT;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index ad112d3..24182cc 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -21,6 +21,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.Timestamp;
@@ -147,12 +148,14 @@
}
/** Returns {@code null} if nothing has been added to {@code oldCollection} */
+ @Nullable
private static ImmutableList<?> getAddedForCollection(
Collection<?> oldCollection, Collection<?> newCollection) {
ImmutableList<?> notInOldCollection = getAdditions(oldCollection, newCollection);
return notInOldCollection.isEmpty() ? null : notInOldCollection;
}
+ @Nullable
private static ImmutableList<Object> getAdditions(
Collection<?> oldCollection, Collection<?> newCollection) {
if (oldCollection == null)
@@ -169,6 +172,7 @@
}
/** Returns {@code null} if nothing has been added to {@code oldMap} */
+ @Nullable
private static ImmutableMap<Object, Object> getAddedForMap(Map<?, ?> oldMap, Map<?, ?> newMap) {
ImmutableMap.Builder<Object, Object> additionsBuilder = ImmutableMap.builder();
for (Map.Entry<?, ?> entry : newMap.entrySet()) {
diff --git a/java/com/google/gerrit/extensions/common/ChangeInput.java b/java/com/google/gerrit/extensions/common/ChangeInput.java
index ea12ef1..55a5883 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInput.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common;
import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.NotifyInfo;
import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -35,6 +36,7 @@
public Boolean newBranch;
public Map<String, String> validationOptions;
public MergeInput merge;
+ public ApplyPatchInput patch;
public AccountInput author;
diff --git a/java/com/google/gerrit/extensions/common/FileInfo.java b/java/com/google/gerrit/extensions/common/FileInfo.java
index c732663..9526fbb 100644
--- a/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -18,6 +18,8 @@
public class FileInfo {
public Character status;
+ public Integer oldMode;
+ public Integer newMode;
public Boolean binary;
public String oldPath;
public Integer linesInserted;
diff --git a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
index d011d5d..180a94691 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
@@ -45,6 +45,16 @@
return check("linesDeleted").that(fileInfo.linesDeleted);
}
+ public IntegerSubject oldMode() {
+ isNotNull();
+ return check("oldMode").that(fileInfo.oldMode);
+ }
+
+ public IntegerSubject newMode() {
+ isNotNull();
+ return check("newMode").that(fileInfo.newMode);
+ }
+
public ComparableSubject<Character> status() {
isNotNull();
return check("status").that(fileInfo.status);
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
index d8dd1f9..7ed7077 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.registration;
+import com.google.gerrit.common.Nullable;
import com.google.inject.Binding;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -39,6 +40,7 @@
return new DynamicItem<>(key, find(injector, type), PluginName.GERRIT);
}
+ @Nullable
private static <T> Provider<T> find(Injector src, TypeLiteral<T> type) {
List<Binding<T>> bindings = src.findBindingsByType(type);
if (bindings != null && bindings.size() == 1) {
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index a0b2c6a..6dc8c6a 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.Nullable;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.Provider;
@@ -313,6 +314,7 @@
return key;
}
+ @Nullable
@Override
public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
Extension<T> n = new Extension<>(item.getPluginName(), newItem);
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index fb520b4..67fc068 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -16,6 +16,7 @@
import static java.util.Objects.requireNonNull;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.Export;
import com.google.inject.Key;
import com.google.inject.Provider;
@@ -79,6 +80,7 @@
return key;
}
+ @Nullable
@Override
public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
if (items.replace(np, item, newItem)) {
diff --git a/java/com/google/gerrit/extensions/restapi/Url.java b/java/com/google/gerrit/extensions/restapi/Url.java
index 9c69376..09def84 100644
--- a/java/com/google/gerrit/extensions/restapi/Url.java
+++ b/java/com/google/gerrit/extensions/restapi/Url.java
@@ -16,6 +16,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.gerrit.common.Nullable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
@@ -40,6 +41,7 @@
* @param component a string containing text to encode.
* @return a string with all invalid URL characters escaped.
*/
+ @Nullable
public static String encode(String component) {
if (component != null) {
try {
@@ -52,6 +54,7 @@
}
/** Decode a URL encoded string, e.g. from {@code "%2F"} to {@code "/"}. */
+ @Nullable
public static String decode(String str) {
if (str != null) {
try {
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index 0ee5212..b2173c4 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/exceptions",
"//java/com/google/gerrit/extensions:api",
diff --git a/java/com/google/gerrit/gpg/PublicKeyChecker.java b/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 0a96212..5347398 100644
--- a/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -30,6 +30,7 @@
import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import java.io.IOException;
import java.time.Instant;
@@ -229,6 +230,7 @@
|| PushCertificateChecker.getCreationTime(revocation).isBefore(now);
}
+ @Nullable
private PGPSignature scanRevocations(
PGPPublicKey key,
Instant now,
@@ -264,6 +266,7 @@
return null;
}
+ @Nullable
private RevocationKey getRevocationKey(PGPPublicKey key, PGPSignature sig) throws PGPException {
if (sig.getKeyID() != key.getKeyID()) {
return null;
@@ -320,6 +323,7 @@
}
}
+ @Nullable
private static RevocationReason getRevocationReason(PGPSignature sig) {
if (sig.getSignatureType() != KEY_REVOCATION) {
throw new IllegalArgumentException(
@@ -425,6 +429,7 @@
return CheckResult.create(OK, problems);
}
+ @Nullable
private static PGPPublicKey getSigner(
PublicKeyStore store,
PGPSignature sig,
@@ -455,6 +460,7 @@
}
}
+ @Nullable
private String checkTrustSubpacket(PGPSignature sig, int depth) {
SignatureSubpacket trustSub =
sig.getHashedSubPackets().getSubpacket(SignatureSubpacketTags.TRUST_SIG);
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 2cce480..d167ac8 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -21,6 +21,7 @@
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.git.ObjectIds;
import java.io.ByteArrayInputStream;
@@ -92,6 +93,7 @@
* null} if none was found.
* @throws PGPException if an error occurred verifying the signature.
*/
+ @Nullable
public static PGPPublicKey getSigner(
Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, byte[] data) throws PGPException {
for (PGPPublicKeyRing kr : keyRings) {
@@ -126,6 +128,7 @@
* {@code null} if none was found.
* @throws PGPException if an error occurred verifying the certification.
*/
+ @Nullable
public static PGPPublicKey getSigner(
Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, String userId, PGPPublicKey key)
throws PGPException {
@@ -210,6 +213,7 @@
* @throws PGPException if an error occurred parsing the key data.
* @throws IOException if an error occurred reading the repository data.
*/
+ @Nullable
public PGPPublicKeyRing get(byte[] fingerprint) throws PGPException, IOException {
List<PGPPublicKeyRing> keyRings = get(Fingerprint.getId(fingerprint), fingerprint);
return !keyRings.isEmpty() ? keyRings.get(0) : null;
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 17ca5a4..b9ff50b 100644
--- a/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -22,6 +22,7 @@
import com.google.common.base.Joiner;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -176,6 +177,7 @@
return CheckResult.ok();
}
+ @Nullable
private PGPSignature readSignature(PushCertificate cert) throws IOException {
ArmoredInputStream in =
new ArmoredInputStream(new ByteArrayInputStream(Constants.encode(cert.getSignature())));
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 3341806..d51ee6a 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -29,6 +29,7 @@
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.EmailException;
import com.google.gerrit.exceptions.StorageException;
@@ -299,6 +300,7 @@
return externalIdKeyFactory.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
}
+ @Nullable
private Account getAccountByExternalId(ExternalId.Key extIdKey) {
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 1284829..0142031 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -38,9 +38,11 @@
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:lang3",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
"//lib/guice:guice-servlet",
+ "//lib/jsoup",
],
)
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 5b62f96..9625039 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -116,6 +116,7 @@
}
}
+ @Nullable
private static String readCookie(HttpServletRequest request) {
Cookie[] all = request.getCookies();
if (all != null) {
@@ -219,6 +220,7 @@
}
}
+ @Nullable
@Override
public String getSessionId() {
return val != null ? val.getSessionId() : null;
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index fdbe6aa..86c514b 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -667,6 +668,7 @@
public void destroy() {}
}
+ @Nullable
private static String getSessionIdOrNull(Provider<WebSession> sessionProvider) {
WebSession session = sessionProvider.get();
if (session.isSignedIn()) {
diff --git a/java/com/google/gerrit/httpd/HtmlDomUtil.java b/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 57f2664..16e0938 100644
--- a/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -16,7 +16,9 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -25,6 +27,8 @@
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -39,6 +43,7 @@
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
+import org.jsoup.parser.Parser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -47,6 +52,8 @@
/** Utility functions to deal with HTML using W3C DOM operations. */
public class HtmlDomUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
/** Standard character encoding we prefer (UTF-8). */
public static final Charset ENC = UTF_8;
@@ -89,6 +96,7 @@
}
/** Find an element by its "id" attribute; null if no element is found. */
+ @Nullable
public static Element find(Node parent, String name) {
NodeList list = parent.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
@@ -139,6 +147,7 @@
}
/** Parse an XHTML file from our CLASSPATH and return the instance. */
+ @Nullable
public static Document parseFile(Class<?> context, String name) throws IOException {
try (InputStream in = context.getResourceAsStream(name)) {
if (in == null) {
@@ -168,6 +177,7 @@
}
/** Read a Read a UTF-8 text file from our CLASSPATH and return it. */
+ @Nullable
public static String readFile(Class<?> context, String name) throws IOException {
try (InputStream in = context.getResourceAsStream(name)) {
if (in == null) {
@@ -180,6 +190,7 @@
}
/** Parse an XHTML file from the local drive and return the instance. */
+ @Nullable
public static Document parseFile(Path path) throws IOException {
try (InputStream in = Files.newInputStream(path)) {
Document doc = newBuilder().parse(in);
@@ -193,6 +204,7 @@
}
/** Read a UTF-8 text file from the local drive. */
+ @Nullable
public static String readFile(Path parentDir, String name) throws IOException {
if (parentDir == null) {
return null;
@@ -215,4 +227,27 @@
factory.setCoalescing(true);
return factory.newDocumentBuilder();
}
+
+ /**
+ * Attaches nonce to all script elements in html.
+ *
+ * <p>The returned html is not guaranteed to have the same formatting as the input.
+ *
+ * @return Updated html or {#link Optional.empty()} if parsing failed.
+ */
+ public static Optional<String> attachNonce(String html, String nonce) {
+ Parser parser = Parser.htmlParser();
+ org.jsoup.nodes.Document document = parser.parseInput(html, "");
+ if (!parser.getErrors().isEmpty()) {
+ logger.atSevere().atMostEvery(5, TimeUnit.MINUTES).log(
+ "Html couldn't be parsed to attach nonce. Errors: %s", parser.getErrors());
+ return Optional.empty();
+ }
+ document.getElementsByTag("script").attr("nonce", nonce);
+ return Optional.of(
+ document
+ .outputSettings(
+ new org.jsoup.nodes.Document.OutputSettings().prettyPrint(false).indentAmount(0))
+ .outerHtml());
+ }
}
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index de6ae50..5a99cab 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -23,6 +23,7 @@
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -226,6 +227,7 @@
}
}
+ @Nullable
private AuthInfo extractAuthInfo(String hdr, String encoding)
throws UnsupportedEncodingException {
byte[] decoded = BaseEncoding.base64().decode(hdr.substring(BASIC.length()));
@@ -241,6 +243,7 @@
defaultAuthProvider);
}
+ @Nullable
private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
String username =
URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
@@ -272,6 +275,7 @@
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
}
+ @Nullable
private static Cookie findGitCookie(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
diff --git a/java/com/google/gerrit/httpd/RemoteUserUtil.java b/java/com/google/gerrit/httpd/RemoteUserUtil.java
index 84954dc..6f3e9c4 100644
--- a/java/com/google/gerrit/httpd/RemoteUserUtil.java
+++ b/java/com/google/gerrit/httpd/RemoteUserUtil.java
@@ -19,6 +19,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.Nullable;
import javax.servlet.http.HttpServletRequest;
public class RemoteUserUtil {
@@ -62,6 +63,7 @@
* @param auth header value which is used for extracting.
* @return username if available or null.
*/
+ @Nullable
public static String extractUsername(String auth) {
auth = emptyToNull(auth);
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index 87bf3a6..1137b65 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -30,6 +30,7 @@
import com.google.common.cache.Cache;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
@@ -149,6 +150,7 @@
return -1;
}
+ @Nullable
Val get(Key key) {
Val val = self.getIfPresent(key.token);
if (val != null && val.expiresAt <= nowMs()) {
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 2f760f0..bc8a01a 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -185,6 +186,7 @@
return account.map(a -> new AuthResult(a.account().id(), null, false));
}
+ @Nullable
private AuthResult auth(Account.Id account) {
if (account != null) {
return new AuthResult(account, null, false);
@@ -192,6 +194,7 @@
return null;
}
+ @Nullable
private AuthResult byUserName(String userName) {
List<AccountState> accountStates = queryProvider.get().byExternalId(SCHEME_USERNAME, userName);
if (accountStates.isEmpty()) {
@@ -223,6 +226,7 @@
}
}
+ @Nullable
private AuthResult create() throws IOException {
try {
return accountManager.authenticate(
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index acb3282..be833ea 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -21,6 +21,7 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.RemoteUserUtil;
@@ -143,6 +144,7 @@
: remoteUser;
}
+ @Nullable
String getRemoteDisplayname(HttpServletRequest req) {
if (displaynameHeader != null) {
String raw = req.getHeader(displaynameHeader);
@@ -151,6 +153,7 @@
return null;
}
+ @Nullable
String getRemoteEmail(HttpServletRequest req) {
if (emailHeader != null) {
return emptyToNull(req.getHeader(emailHeader));
@@ -158,6 +161,7 @@
return null;
}
+ @Nullable
String getRemoteExternalIdToken(HttpServletRequest req) {
if (externalIdHeader != null) {
return emptyToNull(req.getHeader(externalIdHeader));
diff --git a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 0b6008c..e7057ad 100644
--- a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -334,6 +334,7 @@
form.appendChild(div);
}
+ @Nullable
private OAuthServiceProvider lookupOAuthServiceProvider(String providerId) {
if (providerId.startsWith("http://")) {
providerId = providerId.substring("http://".length());
@@ -350,6 +351,7 @@
return null;
}
+ @Nullable
private static String getLastId(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index fcd16ae..0c71d68 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -15,6 +15,7 @@
package com.google.gerrit.httpd.auth.openid;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.auth.openid.OpenIdUrls;
import com.google.gerrit.entities.Account;
@@ -518,6 +519,7 @@
rsp.sendRedirect(rdr.toString());
}
+ @Nullable
private State init(
HttpServletRequest req,
final String openidIdentifier,
diff --git a/java/com/google/gerrit/httpd/auth/restapi/BUILD b/java/com/google/gerrit/httpd/auth/restapi/BUILD
index d499768..9ab51c5 100644
--- a/java/com/google/gerrit/httpd/auth/restapi/BUILD
+++ b/java/com/google/gerrit/httpd/auth/restapi/BUILD
@@ -6,6 +6,7 @@
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/auth",
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/server",
"//lib/flogger:api",
diff --git a/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java b/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java
index 3594c7c..2eee415 100644
--- a/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java
+++ b/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java
@@ -16,6 +16,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.auth.oauth.OAuthTokenCache;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -70,6 +71,7 @@
return Response.ok(accessTokenInfo);
}
+ @Nullable
private static String getHostName(String canonicalWebUrl) {
if (canonicalWebUrl == null) {
logger.atSevere().log(
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index 8b0023b..4270150 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -110,6 +110,7 @@
private final Provider<CurrentUser> userProvider;
private final EnvList _env;
+ @SuppressWarnings("CheckReturnValue")
@Inject
GitwebServlet(
GitRepositoryManager repoManager,
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index a03aa36..9b8f4c6 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -35,6 +35,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.resources.Resource;
import com.google.gerrit.httpd.resources.ResourceKey;
import com.google.gerrit.httpd.resources.SmallResource;
@@ -174,6 +175,7 @@
plugins.put(name, holder);
}
+ @Nullable
private GuiceFilter load(Plugin plugin) {
if (plugin.getHttpInjector() != null) {
final String name = plugin.getName();
@@ -327,6 +329,7 @@
}
}
+ @Nullable
private static Pattern makeAllowOrigin(Config cfg) {
String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
if (allow.length > 0) {
@@ -720,6 +723,7 @@
this.docPrefix = getPrefix(plugin, "Gerrit-HttpDocumentationPrefix", "Documentation/");
}
+ @Nullable
private static String getPrefix(Plugin plugin, String attr, String def) {
Path path = plugin.getSrcFile();
PluginContentScanner scanner = plugin.getContentScanner();
diff --git a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
index fc0ec39..ed29629 100644
--- a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -19,6 +19,7 @@
import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.resources.Resource;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.plugins.Plugin;
@@ -119,6 +120,7 @@
filter.set(guiceFilter);
}
+ @Nullable
private GuiceFilter load(Plugin plugin) {
if (plugin.getHttpInjector() != null) {
final String name = plugin.getName();
diff --git a/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java b/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
index c13286e..3f59084 100644
--- a/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
+++ b/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
@@ -15,15 +15,17 @@
package com.google.gerrit.httpd.raw;
import com.google.common.cache.Cache;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import java.nio.file.Path;
-class DirectoryDocServlet extends ResourceServlet {
+class DirectoryDocServlet extends DocServlet {
private static final long serialVersionUID = 1L;
private final Path doc;
- DirectoryDocServlet(Cache<Path, Resource> cache, Path unpackedWar) {
- super(cache, true);
+ DirectoryDocServlet(
+ Cache<Path, Resource> cache, Path unpackedWar, ExperimentFeatures experimentFeatures) {
+ super(cache, true, experimentFeatures);
this.doc = unpackedWar.resolve("Documentation");
}
diff --git a/java/com/google/gerrit/httpd/raw/DocServlet.java b/java/com/google/gerrit/httpd/raw/DocServlet.java
new file mode 100644
index 0000000..fbae358
--- /dev/null
+++ b/java/com/google/gerrit/httpd/raw/DocServlet.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2016 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.httpd.raw;
+
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+abstract class DocServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ private final ExperimentFeatures experimentFeatures;
+
+ DocServlet(Cache<Path, Resource> cache, boolean refresh, ExperimentFeatures experimentFeatures) {
+ super(cache, refresh);
+ this.experimentFeatures = experimentFeatures;
+ }
+
+ @Override
+ protected boolean shouldProcessResourceBeforeServe(
+ HttpServletRequest req, HttpServletResponse rsp, Path p) {
+ String nonce = (String) req.getAttribute("nonce");
+ if (!experimentFeatures.isFeatureEnabled(GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+ || nonce == null) {
+ return false;
+ }
+ return ResourceServlet.contentType(p.toString()).equals("text/html");
+ }
+
+ @Override
+ protected Resource processResourceBeforeServe(
+ HttpServletRequest req, HttpServletResponse rsp, Resource resource) {
+ // ResourceServlet doesn't set character encoding for a resource. Gerrit will
+ // default to setting charset to utf-8, if none provided. So we guess UTF_8 here.
+ Optional<String> updatedHtml =
+ HtmlDomUtil.attachNonce(
+ new String(resource.raw, StandardCharsets.UTF_8), (String) req.getAttribute("nonce"));
+ if (updatedHtml.isEmpty()) {
+ return resource;
+ }
+ return new Resource(
+ resource.lastModified,
+ resource.contentType,
+ updatedHtml.get().getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 1c6e058..5cf63d9 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -106,6 +106,7 @@
ListChangesOption.SKIP_DIFFSTAT,
ListChangesOption.SUBMIT_REQUIREMENTS);
+ @Nullable
public static String getPath(@Nullable String requestedURL) throws URISyntaxException {
if (requestedURL == null) {
return null;
diff --git a/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 8be4abc..871ec78 100644
--- a/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -126,6 +126,34 @@
*/
protected abstract Path getResourcePath(String pathInfo) throws IOException;
+ /**
+ * Indicates that resource requires some processing before being served.
+ *
+ * <p>If true, the caching headers in response are set to not cache. Additionally, streaming
+ * option is disabled.
+ *
+ * @param req the HTTP servlet request
+ * @param rsp the HTTP servlet response
+ * @param p URL path
+ * @return true if the {@link #processResourceBeforeServe(HttpServletRequest, HttpServletResponse,
+ * Resource)} should be called.
+ */
+ protected boolean shouldProcessResourceBeforeServe(
+ HttpServletRequest req, HttpServletResponse rsp, Path p) {
+ return false;
+ }
+
+ /**
+ * Edits the resource before adding it to the response.
+ *
+ * @param req the HTTP servlet request
+ * @param rsp the HTTP servlet response
+ */
+ protected Resource processResourceBeforeServe(
+ HttpServletRequest req, HttpServletResponse rsp, Resource resource) {
+ return resource;
+ }
+
protected FileTime getLastModifiedTime(Path p) throws IOException {
return Files.getLastModifiedTime(p);
}
@@ -148,10 +176,11 @@
return;
}
+ boolean requiresPostProcess = shouldProcessResourceBeforeServe(req, rsp, p);
Resource r = cache.getIfPresent(p);
try {
if (r == null) {
- if (maybeStream(p, req, rsp)) {
+ if (!requiresPostProcess && maybeStream(p, req, rsp)) {
return; // Bypass cache for large resource.
}
r = cache.get(p, newLoader(p));
@@ -176,11 +205,16 @@
CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_NOT_FOUND);
return;
- } else if (cacheOnClient && r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+ } else if (!requiresPostProcess
+ && cacheOnClient
+ && r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
rsp.setStatus(SC_NOT_MODIFIED);
return;
}
+ if (requiresPostProcess) {
+ r = processResourceBeforeServe(req, rsp, r);
+ }
byte[] tosend = r.raw;
if (!r.contentType.equals(JS) && RequestUtil.acceptsGzipEncoding(req)) {
byte[] gz = HtmlDomUtil.compress(tosend);
@@ -190,7 +224,7 @@
}
}
- if (cacheOnClient) {
+ if (!requiresPostProcess && cacheOnClient) {
rsp.setHeader(ETAG, r.etag);
} else {
CacheHeaders.setNotCacheable(rsp);
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 15dcf42..129d961 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -144,12 +144,13 @@
@Provides
@Singleton
@Named(DOC_SERVLET)
- HttpServlet getDocServlet(@Named(CACHE) Cache<Path, Resource> cache) {
+ HttpServlet getDocServlet(
+ @Named(CACHE) Cache<Path, Resource> cache, ExperimentFeatures experimentFeatures) {
Paths p = getPaths();
if (p.warFs != null) {
- return new WarDocServlet(cache, p.warFs);
+ return new WarDocServlet(cache, p.warFs, experimentFeatures);
} else if (p.unpackedWar != null && !p.isDev()) {
- return new DirectoryDocServlet(cache, p.unpackedWar);
+ return new DirectoryDocServlet(cache, p.unpackedWar, experimentFeatures);
} else {
return new HttpServlet() {
private static final long serialVersionUID = 1L;
@@ -305,6 +306,7 @@
sourceRoot = getSourceRootOrNull();
}
+ @Nullable
private static Path getSourceRootOrNull() {
try {
return GerritLauncher.resolveInSourceRoot(".");
@@ -313,6 +315,7 @@
}
}
+ @Nullable
private FileSystem getDistributionArchive(File war) throws IOException {
if (war == null) {
return null;
@@ -320,6 +323,7 @@
return GerritLauncher.getZipFileSystem(war.toPath());
}
+ @Nullable
private File getLauncherLoadedFrom() {
File war;
try {
@@ -441,6 +445,7 @@
super(req);
}
+ @Nullable
@Override
public String getPathInfo() {
String uri = getRequestURI();
diff --git a/java/com/google/gerrit/httpd/raw/WarDocServlet.java b/java/com/google/gerrit/httpd/raw/WarDocServlet.java
index 27520e3..718d46d 100644
--- a/java/com/google/gerrit/httpd/raw/WarDocServlet.java
+++ b/java/com/google/gerrit/httpd/raw/WarDocServlet.java
@@ -15,20 +15,22 @@
package com.google.gerrit.httpd.raw;
import com.google.common.cache.Cache;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.util.time.TimeUtil;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
-class WarDocServlet extends ResourceServlet {
+class WarDocServlet extends DocServlet {
private static final long serialVersionUID = 1L;
private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
private final FileSystem warFs;
- WarDocServlet(Cache<Path, Resource> cache, FileSystem warFs) {
- super(cache, false);
+ WarDocServlet(
+ Cache<Path, Resource> cache, FileSystem warFs, ExperimentFeatures experimentFeatures) {
+ super(cache, false, experimentFeatures);
this.warFs = warFs;
}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 543e794..23de6db 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -315,6 +315,7 @@
this.cancellationMetrics = cancellationMetrics;
}
+ @Nullable
private static Pattern makeAllowOrigin(Config cfg) {
String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
if (allow.length > 0) {
@@ -854,6 +855,7 @@
}
}
+ @Nullable
private String getEtagWithRetry(
HttpServletRequest req, TraceContext traceContext, RestResource.HasETag rsrc) {
try (TraceTimer ignored =
@@ -1277,6 +1279,7 @@
return ((ParameterizedType) supertype).getActualTypeArguments()[2];
}
+ @Nullable
private Object parseRequest(HttpServletRequest req, Type type)
throws IOException, BadRequestException, SecurityException, IllegalArgumentException,
NoSuchMethodException, IllegalAccessException, InstantiationException,
diff --git a/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java b/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
index 655f4ca..2065a31 100644
--- a/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
+++ b/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
@@ -18,6 +18,7 @@
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
@@ -125,6 +126,7 @@
return cssFile.isStale() || headerFile.isStale() || footerFile.isStale();
}
+ @Nullable
private static Element readXml(FileInfo src) throws IOException {
Document d = HtmlDomUtil.parseFile(src.path);
return d != null ? d.getDocumentElement() : null;
diff --git a/java/com/google/gerrit/index/IndexedField.java b/java/com/google/gerrit/index/IndexedField.java
index e44f562..5fcb062 100644
--- a/java/com/google/gerrit/index/IndexedField.java
+++ b/java/com/google/gerrit/index/IndexedField.java
@@ -118,7 +118,7 @@
/**
* Defines how {@link IndexedField} can be searched and how the index tokens are generated.
*
- * <p>Multiple {@link SearchSpec} can be defined on single {@link IndexedField}.
+ * <p>Multiple {@link SearchSpec} can be defined on a single {@link IndexedField}.
*
* <p>Depending on the implementation, indexes can choose to store {@link IndexedField} and {@link
* SearchSpec} separately. The searches are issues to {@link SearchSpec}.
@@ -357,7 +357,11 @@
private Map<String, SearchSpec> searchSpecs = new HashMap<>();
- /** The name to store this field under. */
+ /**
+ * The name to store this field under.
+ *
+ * <p>The name should use the UpperCamelCase format, see {@link Builder#checkName}.
+ */
public abstract String name();
/** Optional description of the field data. */
@@ -375,6 +379,8 @@
/**
* Optional size constrain on the field. The size is not constrained if this property is {@link
* Optional#empty()}
+ *
+ * <p>If the field is {@link #repeatable()}, the constraint applies to each element separately.
*/
public abstract Optional<Integer> size();
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 403be35..9f07cab 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -202,7 +202,7 @@
* @return all fields in this schema indexed by name.
*/
public final ImmutableMap<String, SchemaField<T, ?>> getSchemaFields() {
- return ImmutableMap.copyOf(schemaFields);
+ return schemaFields;
}
public final ImmutableMap<String, IndexedField<T, ?>> getIndexFields() {
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index f7feaa0..f5f30bd 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -62,7 +62,7 @@
int minCardinality = Integer.MAX_VALUE;
for (Predicate<T> p : getChildren()) {
if (p instanceof DataSource) {
- DataSource<T> source = (DataSource<T>) p;
+ DataSource<?> source = (DataSource<?>) p;
int cardinality = source.getCardinality();
c = Math.min(c, source.getCardinality());
@@ -121,7 +121,7 @@
@SuppressWarnings("unchecked")
private PaginatingSource<T> toPaginatingSource(Predicate<T> pred) {
- return new PaginatingSource<T>((DataSource<T>) pred, start, indexConfig) {
+ return new PaginatingSource<>((DataSource<T>) pred, start, indexConfig) {
@Override
protected boolean match(T object) {
return AndSource.this.match(object);
diff --git a/java/com/google/gerrit/index/query/FieldBundle.java b/java/com/google/gerrit/index/query/FieldBundle.java
index 60881df..551de92 100644
--- a/java/com/google/gerrit/index/query/FieldBundle.java
+++ b/java/com/google/gerrit/index/query/FieldBundle.java
@@ -16,9 +16,12 @@
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
+import com.google.gerrit.index.IndexedField;
+import com.google.gerrit.index.IndexedField.SearchSpec;
import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
/** FieldBundle is an abstraction that allows retrieval of raw values from different sources. */
@@ -27,8 +30,22 @@
// Map String => List{Integer, Long, Timestamp, String, byte[]}
private ImmutableListMultimap<String, Object> fields;
- public FieldBundle(ListMultimap<String, Object> fields) {
+ /**
+ * Depending on the index implementation 1) either {@link IndexedField} are stored once and
+ * referenced by {@link com.google.gerrit.index.IndexedField.SearchSpec} on the queries, 2) or
+ * each {@link com.google.gerrit.index.IndexedField.SearchSpec} is stored individually.
+ *
+ * <p>In case #1 {@link #storesIndexedFields} is set to {@code true} and the {@link #fields}
+ * contain a map from {@link IndexedField#name()} to a stored value.
+ *
+ * <p>In case #2 {@link #storesIndexedFields} is set to {@code false} and the {@link #fields}
+ * contain a map from {@link SearchSpec#name()} to a stored value.
+ */
+ private final boolean storesIndexedFields;
+
+ public FieldBundle(ListMultimap<String, Object> fields, boolean storesIndexedFields) {
this.fields = ImmutableListMultimap.copyOf(fields);
+ this.storesIndexedFields = storesIndexedFields;
}
/**
@@ -46,13 +63,17 @@
@SuppressWarnings("unchecked")
public <T> T getValue(SchemaField<?, T> schemaField) {
checkArgument(schemaField.isStored(), "Field must be stored");
+ String storedFieldName =
+ storesIndexedFields && schemaField instanceof IndexedField<?, ?>.SearchSpec
+ ? ((IndexedField<?, ?>.SearchSpec) schemaField).getField().name()
+ : schemaField.getName();
checkArgument(
- fields.containsKey(schemaField.getName()) || schemaField.isRepeatable(),
+ fields.containsKey(storedFieldName) || schemaField.isRepeatable(),
"Field %s is not in result set %s",
- schemaField.getName(),
+ storedFieldName,
fields.keySet());
- Iterable<Object> result = fields.get(schemaField.getName());
+ ImmutableList<Object> result = fields.get(storedFieldName);
if (schemaField.isRepeatable()) {
return (T) result;
}
diff --git a/java/com/google/gerrit/index/query/IntegerRangePredicate.java b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
index 850c4a5..278d2af 100644
--- a/java/com/google/gerrit/index/query/IntegerRangePredicate.java
+++ b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
@@ -14,13 +14,13 @@
package com.google.gerrit.index.query;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.RangeUtil.Range;
public abstract class IntegerRangePredicate<T> extends IndexPredicate<T> {
private final Range range;
- protected IntegerRangePredicate(FieldDef<T, Integer> type, String value)
+ protected IntegerRangePredicate(SchemaField<T, Integer> type, String value)
throws QueryParseException {
super(type, value);
range = RangeUtil.getRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE);
diff --git a/java/com/google/gerrit/index/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index 5c003bc..b6418a9 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -20,12 +20,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexCollection;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
@@ -76,10 +77,10 @@
}
@SafeVarargs
- public final Q setRequestedFields(FieldDef<T, ?>... fields) {
+ public final Q setRequestedFields(SchemaField<T, ?>... fields) {
checkArgument(fields.length > 0, "requested field list is empty");
queryProcessor.setRequestedFields(
- Arrays.stream(fields).map(FieldDef::getName).collect(toSet()));
+ Arrays.stream(fields).map(SchemaField::getName).collect(toSet()));
return self();
}
@@ -118,6 +119,7 @@
}
}
+ @Nullable
protected final Schema<T> schema() {
Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
return index != null ? index.getSchema() : null;
diff --git a/java/com/google/gerrit/index/query/LimitPredicate.java b/java/com/google/gerrit/index/query/LimitPredicate.java
index 23e0f6d..9196811 100644
--- a/java/com/google/gerrit/index/query/LimitPredicate.java
+++ b/java/com/google/gerrit/index/query/LimitPredicate.java
@@ -14,8 +14,11 @@
package com.google.gerrit.index.query;
+import com.google.gerrit.common.Nullable;
+
public class LimitPredicate<T> extends IntPredicate<T> implements Matchable<T> {
@SuppressWarnings("unchecked")
+ @Nullable
public static Integer getLimit(String fieldName, Predicate<?> p) {
IntPredicate<?> ip = QueryBuilder.find(p, IntPredicate.class, fieldName);
return ip != null ? ip.intValue() : null;
diff --git a/java/com/google/gerrit/index/query/PaginatingSource.java b/java/com/google/gerrit/index/query/PaginatingSource.java
index fd3a218..337332f 100644
--- a/java/com/google/gerrit/index/query/PaginatingSource.java
+++ b/java/com/google/gerrit/index/query/PaginatingSource.java
@@ -104,7 +104,7 @@
@Override
public ResultSet<FieldBundle> readRaw() {
- // TOOD(hiesel): Implement
+ // TODO(hiesel): Implement
throw new UnsupportedOperationException("not implemented");
}
@@ -113,6 +113,12 @@
.transformAndConcat(this::transformBuffer);
}
+ /**
+ * Checks whether the given object matches.
+ *
+ * @param object the object to be matched
+ * @return whether the given object matches
+ */
protected boolean match(T object) {
return true;
}
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 959694b..f237006 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -282,7 +282,7 @@
@SuppressWarnings("unchecked")
DataSource<T> s = (DataSource<T>) pred;
if (initialPageSize < limit && !(pred instanceof AndSource)) {
- s = new PaginatingSource<T>(s, start, indexConfig);
+ s = new PaginatingSource<>(s, start, indexConfig);
}
sources.add(s);
}
diff --git a/java/com/google/gerrit/index/query/RegexPredicate.java b/java/com/google/gerrit/index/query/RegexPredicate.java
index 60a2a9e..4c76770 100644
--- a/java/com/google/gerrit/index/query/RegexPredicate.java
+++ b/java/com/google/gerrit/index/query/RegexPredicate.java
@@ -14,14 +14,14 @@
package com.google.gerrit.index.query;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
public abstract class RegexPredicate<I> extends IndexPredicate<I> {
- protected RegexPredicate(FieldDef<I, ?> def, String value) {
+ protected RegexPredicate(SchemaField<I, ?> def, String value) {
super(def, value);
}
- protected RegexPredicate(FieldDef<I, ?> def, String name, String value) {
+ protected RegexPredicate(SchemaField<I, ?> def, String name, String value) {
super(def, name, value);
}
}
diff --git a/java/com/google/gerrit/index/query/TimestampRangePredicate.java b/java/com/google/gerrit/index/query/TimestampRangePredicate.java
index 29d6f22..1fd81a6 100644
--- a/java/com/google/gerrit/index/query/TimestampRangePredicate.java
+++ b/java/com/google/gerrit/index/query/TimestampRangePredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.index.query;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.json.JavaSqlTimestampHelper;
import java.sql.Timestamp;
import java.time.Instant;
@@ -30,7 +30,7 @@
}
}
- protected TimestampRangePredicate(FieldDef<I, Timestamp> def, String name, String value) {
+ protected TimestampRangePredicate(SchemaField<I, Timestamp> def, String name, String value) {
super(def, name, value);
}
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 50efef0..6f79dce 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -151,6 +151,7 @@
@Override
public ResultSet<V> read() {
return new ListResultSet<>(results) {
+ @Nullable
@Override
public Object searchAfter() {
@Nullable V last = Iterables.getLast(results, null);
@@ -175,7 +176,7 @@
fields.put(field.getName(), field.get(result));
}
}
- fieldBundles.add(new FieldBundle(fields.build()));
+ fieldBundles.add(new FieldBundle(fields.build(), /* storesIndexedFields= */ false));
searchAfter = keyFor(result);
}
ImmutableList<FieldBundle> resultSet = fieldBundles.build();
@@ -265,7 +266,7 @@
protected ChangeData valueFor(Map<String, Object> doc) {
ChangeData cd =
changeDataFactory.create(
- Project.nameKey((String) doc.get(ChangeField.PROJECT.getName())),
+ Project.nameKey((String) doc.get(ChangeField.PROJECT_SPEC.getName())),
Change.id(Integer.valueOf((String) doc.get(ChangeField.LEGACY_ID_STR.getName()))));
for (SchemaField<ChangeData, ?> field : getSchema().getSchemaFields().values()) {
field.setIfPossible(cd, new FakeStoredValue(doc.get(field.getName())));
diff --git a/java/com/google/gerrit/index/testing/TestIndexedFields.java b/java/com/google/gerrit/index/testing/TestIndexedFields.java
index f80b8a1..7a120b7 100644
--- a/java/com/google/gerrit/index/testing/TestIndexedFields.java
+++ b/java/com/google/gerrit/index/testing/TestIndexedFields.java
@@ -182,7 +182,10 @@
public static final IndexedField<TestIndexedData, Entities.Change> STORED_PROTO_FIELD =
IndexedField.<TestIndexedData, Entities.Change>builder(
- "TestChange", new TypeToken<Entities.Change>() {})
+ "TestChange",
+ new TypeToken<Entities.Change>() {
+ private static final long serialVersionUID = 1L;
+ })
.stored()
.build(getter(), setter(), ChangeProtoConverter.INSTANCE);
@@ -192,7 +195,10 @@
public static final IndexedField<TestIndexedData, Iterable<Entities.Change>>
ITERABLE_STORED_PROTO_FIELD =
IndexedField.<TestIndexedData, Iterable<Entities.Change>>builder(
- "IterableTestChange", new TypeToken<Iterable<Entities.Change>>() {})
+ "IterableTestChange",
+ new TypeToken<Iterable<Entities.Change>>() {
+ private static final long serialVersionUID = 1L;
+ })
.stored()
.build(getter(), setter(), ChangeProtoConverter.INSTANCE);
diff --git a/java/com/google/gerrit/json/EnumTypeAdapterFactory.java b/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
index 9c32aa8..b6cb5f9 100644
--- a/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
+++ b/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
@@ -14,6 +14,7 @@
package com.google.gerrit.json;
+import com.google.gerrit.common.Nullable;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
@@ -34,6 +35,7 @@
public class EnumTypeAdapterFactory implements TypeAdapterFactory {
@SuppressWarnings({"rawtypes", "unchecked"})
+ @Nullable
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
TypeAdapter<T> defaultEnumAdapter = TypeAdapters.ENUM_FACTORY.create(gson, typeToken);
diff --git a/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java b/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java
index d35b8fb..2557515 100644
--- a/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java
+++ b/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java
@@ -45,6 +45,7 @@
TypeToken.get(SubmitRequirementExpressionResult.class);
@SuppressWarnings({"unchecked"})
+ @Nullable
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (typeToken.equals(OPTIONAL_SR_EXPRESSION_RESULT_TOKEN)) {
diff --git a/java/com/google/gerrit/json/SqlTimestampDeserializer.java b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
index e1cf382..9aeda2b 100644
--- a/java/com/google/gerrit/json/SqlTimestampDeserializer.java
+++ b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
@@ -14,6 +14,7 @@
package com.google.gerrit.json;
+import com.google.gerrit.common.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
@@ -30,6 +31,7 @@
class SqlTimestampDeserializer implements JsonDeserializer<Timestamp>, JsonSerializer<Timestamp> {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+ @Nullable
@Override
public Timestamp deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index a190ebf..6be78d9 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -533,6 +533,7 @@
return myHome;
}
+ @SuppressWarnings("ReturnMissingNullable")
private static File tmproot() {
File tmp;
String gerritTemp = System.getenv("GERRIT_TMP");
@@ -572,6 +573,7 @@
}
}
+ @SuppressWarnings("ReturnMissingNullable")
private static File locateHomeDirectory() {
// Try to find the user's home directory. If we can't find it
// return null so the JVM's default temporary directory is used
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 7b47248..6eac6f7 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -400,7 +400,7 @@
throw FieldType.badFieldType(type);
}
}
- return new FieldBundle(rawFields);
+ return new FieldBundle(rawFields, /* storesIndexedFields= */ false);
}
private static Field.Store store(SchemaField<?, ?> f) {
@@ -542,7 +542,7 @@
}
}
ScoreDoc searchAfter = scoreDoc;
- return new ListResultSet<T>(b.build()) {
+ return new ListResultSet<>(b.build()) {
@Override
public Object searchAfter() {
return searchAfter;
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index ce50473..f7b1f2c 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -125,7 +125,7 @@
} else if (f == ChangeField.UPDATED) {
long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
doc.add(new NumericDocValuesField(UPDATED_SORT_FIELD, t));
- } else if (f == ChangeField.MERGED_ON) {
+ } else if (f == ChangeField.MERGED_ON_SPEC) {
long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
doc.add(new NumericDocValuesField(MERGED_ON_SORT_FIELD, t));
}
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index c28e948b..4122181 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -18,7 +18,7 @@
import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
-import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
import static java.util.Objects.requireNonNull;
@@ -33,6 +33,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.converter.ChangeProtoConverter;
@@ -103,7 +104,7 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED);
- static final String MERGED_ON_SORT_FIELD = sortFieldName(ChangeField.MERGED_ON);
+ static final String MERGED_ON_SORT_FIELD = sortFieldName(ChangeField.MERGED_ON_SPEC);
static final String ID_STR_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID_STR);
private static final String CHANGES = "changes";
@@ -112,7 +113,7 @@
private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
static Term idTerm(ChangeData cd) {
- return idTerm(cd.getVirtualId());
+ return idTerm(cd.getId());
}
static Term idTerm(Change.Id id) {
@@ -454,11 +455,13 @@
* @param subIndex change sub-index
* @return the score doc that can be used to page result sets
*/
+ @Nullable
private ScoreDoc getSearchAfter(ChangeSubIndex subIndex) {
if (isSearchAfterPagination
&& opts.searchAfter() != null
- && opts.searchAfter() instanceof Map) {
- return ((Map<ChangeSubIndex, ScoreDoc>) opts.searchAfter()).get(subIndex);
+ && opts.searchAfter() instanceof Map
+ && ((Map<?, ?>) opts.searchAfter()).get(subIndex) instanceof ScoreDoc) {
+ return (ScoreDoc) ((Map<?, ?>) opts.searchAfter()).get(subIndex);
}
return null;
}
@@ -547,7 +550,7 @@
Change.Id id = Change.id(Integer.valueOf(f.stringValue()));
// IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
- IndexableField project = doc.get(PROJECT.getName()).iterator().next();
+ IndexableField project = doc.get(PROJECT_SPEC.getName()).iterator().next();
cd = changeDataFactory.create(Project.nameKey(project.stringValue()), id);
}
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index d475ab7..b741042 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.index.group.GroupField.UUID_FIELD_SPEC;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.exceptions.StorageException;
@@ -151,6 +152,7 @@
new Sort(new SortField(UUID_SORT_FIELD, SortField.Type.STRING, false)));
}
+ @Nullable
@Override
protected InternalGroup fromDocument(Document doc) {
AccountGroup.UUID uuid =
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 96b22db..6b2b693 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.index.project.ProjectField.NAME;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.QueryOptions;
@@ -151,6 +152,7 @@
new Sort(new SortField(NAME_SORT_FIELD, SortField.Type.STRING, false)));
}
+ @Nullable
@Override
protected ProjectData fromDocument(Document doc) {
Project.NameKey nameKey = Project.nameKey(doc.getField(NAME.getName()).stringValue());
diff --git a/java/com/google/gerrit/lucene/LuceneStoredValue.java b/java/com/google/gerrit/lucene/LuceneStoredValue.java
index 1f8c039..58ae3e0 100644
--- a/java/com/google/gerrit/lucene/LuceneStoredValue.java
+++ b/java/com/google/gerrit/lucene/LuceneStoredValue.java
@@ -38,6 +38,7 @@
this.field = field;
}
+ @Nullable
@Override
public String asString() {
return Iterables.getFirst(asStrings(), null);
@@ -48,6 +49,7 @@
return field.stream().map(f -> f.stringValue()).collect(toImmutableList());
}
+ @Nullable
@Override
public Integer asInteger() {
return Iterables.getFirst(asIntegers(), null);
@@ -58,6 +60,7 @@
return field.stream().map(f -> f.numericValue().intValue()).collect(toImmutableList());
}
+ @Nullable
@Override
public Long asLong() {
return Iterables.getFirst(asLongs(), null);
@@ -68,11 +71,13 @@
return field.stream().map(f -> f.numericValue().longValue()).collect(toImmutableList());
}
+ @Nullable
@Override
public Timestamp asTimestamp() {
return asLong() == null ? null : new Timestamp(asLong());
}
+ @Nullable
@Override
public byte[] asByteArray() {
return Iterables.getFirst(asByteArrays(), null);
diff --git a/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
index c164b29..56cb220 100644
--- a/java/com/google/gerrit/lucene/WrappableSearcherManager.java
+++ b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -17,6 +17,7 @@
* limitations under the License.
*/
+import com.google.gerrit.common.Nullable;
import java.io.IOException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.FilterDirectoryReader;
@@ -132,6 +133,7 @@
reference.getIndexReader().decRef();
}
+ @Nullable
@Override
protected IndexSearcher refreshIfNeeded(IndexSearcher referenceToRefresh) throws IOException {
final IndexReader r = referenceToRefresh.getIndexReader();
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index 5f4e0c0..0f80a0c 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/lifecycle",
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
index d59a1d9..27e9377 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
@@ -23,6 +23,7 @@
import com.codahale.metrics.Timer;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import java.util.ArrayList;
@@ -144,6 +145,7 @@
}
}
+ @Nullable
private static Boolean toBool(ImmutableMap<String, String> atts, String key) {
return Description.TRUE_VALUE.equals(atts.get(key)) ? true : null;
}
diff --git a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
index ef0ced6..84f2320 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
@@ -15,6 +15,7 @@
package com.google.gerrit.metrics.proc;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.sun.management.UnixOperatingSystemMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
@@ -25,6 +26,7 @@
private OperatingSystemMXBeanFactory() {}
+ @Nullable
static OperatingSystemMXBeanInterface create() {
OperatingSystemMXBean sys = ManagementFactory.getOperatingSystemMXBean();
if (sys instanceof UnixOperatingSystemMXBean) {
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index c4e185d..762d988 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -36,6 +36,7 @@
import com.google.gerrit.server.cache.CacheInfo;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.index.options.AutoFlush;
@@ -175,6 +176,8 @@
}
boolean replica = ReplicaUtil.isReplica(globalConfig);
List<Module> modules = new ArrayList<>();
+ modules.add(new WorkQueueModule());
+
Module indexModule;
IndexType indexType = IndexModule.getIndexType(dbInjector);
if (indexType.isLucene()) {
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 824a9a7..6dec2d8 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -19,6 +19,7 @@
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.SiteLibraryLoaderUtil;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.server.config.SitePaths;
@@ -185,6 +186,7 @@
}
}
+ @Nullable
private Path findJarWithSecureStore(SitePaths sitePaths, String secureStoreClass)
throws IOException {
List<Path> jars = SiteLibraryLoaderUtil.listJars(sitePaths.lib_dir);
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 4592cbb..b59b924 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -22,6 +22,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Die;
import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.IndexType;
import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -175,6 +176,7 @@
*/
protected void afterInit(SiteRun run) throws Exception {}
+ @Nullable
protected List<String> getInstallPlugins() {
try {
if (pluginsToInstall != null && pluginsToInstall.isEmpty()) {
@@ -304,6 +306,7 @@
return ConsoleUI.getInstance(false);
}
+ @Nullable
private SecureStoreInitData discoverSecureStoreClass() {
String secureStore = getSecureStoreLib();
if (Strings.isNullOrEmpty(secureStore)) {
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 4e854b5..3dce974 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -17,6 +17,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.InternalGroup;
@@ -182,6 +183,7 @@
return email;
}
+ @Nullable
private AccountSshKey readSshKey(Account.Id id) throws IOException {
String defaultPublicSshKeyFile = "";
Path defaultPublicSshKeyPath = Paths.get(System.getProperty("user.home"), ".ssh", "id_rsa.pub");
diff --git a/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index a7f9c5d..16c4ce7 100644
--- a/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -16,6 +16,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InitStep;
@@ -62,6 +63,7 @@
return pluginsInitSteps;
}
+ @Nullable
private InitStep loadInitStep(Path jar) {
try {
URLClassLoader pluginLoader =
diff --git a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index dffdde7..7666076 100644
--- a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -17,6 +17,7 @@
import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.FormatString;
import com.google.gerrit.common.Die;
+import com.google.gerrit.common.Nullable;
import java.io.Console;
import java.util.EnumSet;
import java.util.Set;
@@ -179,6 +180,7 @@
@Override
@FormatMethod
+ @Nullable
public String password(String fmt, Object... args) {
final String prompt = String.format(fmt, args);
for (; ; ) {
diff --git a/java/com/google/gerrit/pgm/init/api/InitUtil.java b/java/com/google/gerrit/pgm/init/api/InitUtil.java
index d038de7..7688728 100644
--- a/java/com/google/gerrit/pgm/init/api/InitUtil.java
+++ b/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -18,6 +18,7 @@
import com.google.common.io.ByteStreams;
import com.google.gerrit.common.Die;
+import com.google.gerrit.common.Nullable;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
@@ -127,6 +128,7 @@
}
}
+ @Nullable
private static InputStream open(Class<?> sibling, String name) {
final InputStream in = sibling.getResourceAsStream(name);
if (in == null) {
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index b5d35f4..5cc4b5d 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -166,6 +166,7 @@
return nv;
}
+ @Nullable
public String password(String username, String password) {
final String ov = getSecure(password);
diff --git a/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 0a41db5..de08116 100644
--- a/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm.rules;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.Version;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -182,6 +183,7 @@
}
}
+ @Nullable
private String getMyClasspath() {
StringBuilder cp = new StringBuilder();
appendClasspath(cp, getClass().getClassLoader());
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 05b50da..5bffce7 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -90,6 +90,7 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
import com.google.gerrit.server.query.change.DistinctVotersPredicate;
+import com.google.gerrit.server.query.change.HasSubmoduleUpdatePredicate;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.DefaultSubmitRule.DefaultSubmitRuleModule;
import com.google.gerrit.server.rules.IgnoreSelfApprovalRule.IgnoreSelfApprovalRuleModule;
@@ -198,6 +199,7 @@
factory(ChangeData.AssistedFactory.class);
factory(ChangeIsVisibleToPredicate.Factory.class);
factory(DistinctVotersPredicate.Factory.class);
+ factory(HasSubmoduleUpdatePredicate.Factory.class);
factory(ProjectState.Factory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD
index 0a15fda..a5c8b77 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["common/**/*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//lib:guava",
"//lib:jgit",
"//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/prettify/common/SparseFileContent.java b/java/com/google/gerrit/prettify/common/SparseFileContent.java
index 1249b65..f40222a 100644
--- a/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -17,6 +17,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
/**
* A class to store subset of a file's lines in a memory efficient way. Internally, it stores lines
@@ -134,6 +135,7 @@
return getSize();
}
+ @Nullable
private String getLine(int idx) {
// Most requests are sequential in nature, fetching the next
// line from the current range, or the next range.
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 8198ce4..285657e 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -104,6 +104,7 @@
return PatchSet.id(changeId, comment.key.patchSetId);
}
+ @Nullable
public static String extractMessageId(@Nullable String tag) {
if (tag == null || !tag.startsWith("mailMessageId=")) {
return null;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 0f5629e..9ac85e0 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -32,7 +32,6 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.git.GitUpdateFailureException;
@@ -80,6 +79,7 @@
public abstract static class StarField {
private static final String SEPARATOR = ":";
+ @Nullable
public static StarField parse(String s) {
int p = s.indexOf(SEPARATOR);
if (p >= 0) {
@@ -194,7 +194,7 @@
}
}
- public void star(Account.Id accountId, Project.NameKey project, Change.Id changeId, Operation op)
+ public void star(Account.Id accountId, Change.Id changeId, Operation op)
throws IllegalLabelException {
try (Repository repo = repoManager.openRepository(allUsers)) {
String refName = RefNames.refsStarredChanges(changeId, accountId);
diff --git a/java/com/google/gerrit/server/account/AccountLimits.java b/java/com/google/gerrit/server/account/AccountLimits.java
index 5549d28..d97563a 100644
--- a/java/com/google/gerrit/server/account/AccountLimits.java
+++ b/java/com/google/gerrit/server/account/AccountLimits.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.PermissionRange;
import com.google.gerrit.entities.PermissionRule;
@@ -105,6 +106,7 @@
}
/** The range of permitted values associated with a label permission. */
+ @Nullable
public PermissionRange getRange(String permission) {
if (GlobalCapability.hasRange(permission)) {
return toRange(permission, getRules(permission));
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 8824d56..65eb332 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -559,6 +559,11 @@
return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::allVisible);
}
+ public Result resolveIncludeInactiveIgnoreVisibility(String input)
+ throws ConfigInvalidException, IOException {
+ return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::allVisible);
+ }
+
public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::isActive);
}
diff --git a/java/com/google/gerrit/server/account/AuthRequest.java b/java/com/google/gerrit/server/account/AuthRequest.java
index cceda70..b4fbcdb 100644
--- a/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/java/com/google/gerrit/server/account/AuthRequest.java
@@ -102,6 +102,7 @@
return externalId;
}
+ @Nullable
public String getLocalUser() {
if (externalId.isScheme(SCHEME_GERRIT)) {
return externalId.id();
diff --git a/java/com/google/gerrit/server/account/CreateGroupArgs.java b/java/com/google/gerrit/server/account/CreateGroupArgs.java
index ba58c3f..9d9fe9d 100644
--- a/java/com/google/gerrit/server/account/CreateGroupArgs.java
+++ b/java/com/google/gerrit/server/account/CreateGroupArgs.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import java.util.Collection;
@@ -30,6 +31,7 @@
return groupName;
}
+ @Nullable
public String getGroupName() {
return groupName != null ? groupName.get() : null;
}
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index 329825f..cfffceb 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,6 +16,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.AccountFieldName;
@@ -79,6 +80,7 @@
@Override
public void onCreateAccount(AuthRequest who, Account account) {}
+ @Nullable
@Override
public Account.Id lookup(String accountName) throws IOException {
if (emailExpander.canExpand(accountName)) {
diff --git a/java/com/google/gerrit/server/account/DestinationList.java b/java/com/google/gerrit/server/account/DestinationList.java
index 15c1e25..084a3ac 100644
--- a/java/com/google/gerrit/server/account/DestinationList.java
+++ b/java/com/google/gerrit/server/account/DestinationList.java
@@ -18,6 +18,7 @@
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.git.ValidationError;
@@ -39,6 +40,7 @@
destinations.replaceValues(label, toSet(parse(text, DIR_NAME + label, TRIM, null, errors)));
}
+ @Nullable
String asText(String label) {
Set<BranchNameKey> dests = destinations.get(label);
if (dests == null) {
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index eaec9ba..2d947ba 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.RefNames;
@@ -346,6 +347,7 @@
return Protos.toByteArray(InternalGroupSerializer.serialize(value));
}
+ @Nullable
@Override
public InternalGroup deserialize(byte[] in) {
if (Strings.fromByteArray(in).isEmpty()) {
diff --git a/java/com/google/gerrit/server/account/InternalGroupBackend.java b/java/com/google/gerrit/server/account/InternalGroupBackend.java
index 91fe701..01254a0 100644
--- a/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -17,6 +17,7 @@
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
@@ -60,6 +61,7 @@
return ObjectId.isId(uuid.get()); // [0-9a-f]{40};
}
+ @Nullable
@Override
public GroupDescription.Internal get(AccountGroup.UUID uuid) {
if (!handles(uuid)) {
diff --git a/java/com/google/gerrit/server/account/ProjectWatches.java b/java/com/google/gerrit/server/account/ProjectWatches.java
index 42137c1..86132d3 100644
--- a/java/com/google/gerrit/server/account/ProjectWatches.java
+++ b/java/com/google/gerrit/server/account/ProjectWatches.java
@@ -201,6 +201,7 @@
@AutoValue
public abstract static class NotifyValue {
+ @Nullable
public static NotifyValue parse(
Account.Id accountId,
String project,
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 1587bc5..476ca79 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -130,6 +130,7 @@
return true;
}
+ @Nullable
@Override
public GroupDescription.Basic get(AccountGroup.UUID uuid) {
if (uuid == null) {
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 555a2c1..1fce3d5 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -20,6 +20,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.InvalidSshKeyException;
@@ -194,6 +195,7 @@
* @return the SSH key, <code>null</code> if there is no SSH key with this sequence number, or if
* the SSH key with this sequence number has been deleted
*/
+ @Nullable
private AccountSshKey getKey(int seq) {
checkLoaded();
return keys.get(seq - 1).orElse(null);
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index e718bcb..14aa368 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -45,6 +45,13 @@
return new AutoValue_AllExternalIds(byKey.build(), byAccount.build(), byEmail.build());
}
+ static AllExternalIds create(
+ ImmutableMap<ExternalId.Key, ExternalId> byKey,
+ ImmutableSetMultimap<Account.Id, ExternalId> byAccount,
+ ImmutableSetMultimap<String, ExternalId> byEmail) {
+ return new AutoValue_AllExternalIds(byKey, byAccount, byEmail);
+ }
+
public abstract ImmutableMap<ExternalId.Key, ExternalId> byKey();
public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount();
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
index 27672bd..1edb284 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
@@ -253,7 +253,7 @@
}
}
}
- return new AutoValue_AllExternalIds(byKey.build(), byAccount.build(), byEmail.build());
+ return AllExternalIds.create(byKey.build(), byAccount.build(), byEmail.build());
}
private AllExternalIds reloadAllExternalIds(ObjectId notesRev)
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index b0618ba..48c403c 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -1020,6 +1020,7 @@
* @return the external ID that was removed, {@code null} if no external ID with the specified key
* exists
*/
+ @Nullable
private ExternalId remove(
RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
throws IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index b23782f..828f868 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import static javax.servlet.http.HttpServletResponse.SC_OK;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.accounts.AgreementInput;
@@ -575,6 +576,7 @@
}
}
+ @Nullable
@Override
public String generateHttpPassword() throws RestApiException {
HttpPasswordInput input = new HttpPasswordInput();
@@ -589,6 +591,7 @@
}
}
+ @Nullable
@Override
public String setHttpPassword(String password) throws RestApiException {
HttpPasswordInput input = new HttpPasswordInput();
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 1713171..e0569f4 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.AbandonInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchPatchSetInput;
import com.google.gerrit.extensions.api.changes.AssigneeInput;
import com.google.gerrit.extensions.api.changes.AttentionSetApi;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
@@ -69,6 +70,7 @@
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.restapi.change.Abandon;
import com.google.gerrit.server.restapi.change.AddToAttentionSet;
+import com.google.gerrit.server.restapi.change.ApplyPatch;
import com.google.gerrit.server.restapi.change.AttentionSet;
import com.google.gerrit.server.restapi.change.ChangeIncludedIn;
import com.google.gerrit.server.restapi.change.ChangeMessages;
@@ -139,6 +141,7 @@
private final RevertSubmission revertSubmission;
private final Restore restore;
private final CreateMergePatchSet updateByMerge;
+ private final ApplyPatch applyPatch;
private final Provider<SubmittedTogether> submittedTogether;
private final Rebase.CurrentRevision rebase;
private final DeleteChange deleteChange;
@@ -191,6 +194,7 @@
RevertSubmission revertSubmission,
Restore restore,
CreateMergePatchSet updateByMerge,
+ ApplyPatch applyPatch,
Provider<SubmittedTogether> submittedTogether,
Rebase.CurrentRevision rebase,
DeleteChange deleteChange,
@@ -241,6 +245,7 @@
this.abandon = abandon;
this.restore = restore;
this.updateByMerge = updateByMerge;
+ this.applyPatch = applyPatch;
this.submittedTogether = submittedTogether;
this.rebase = rebase;
this.deleteChange = deleteChange;
@@ -389,6 +394,15 @@
}
@Override
+ public ChangeInfo applyPatch(ApplyPatchPatchSetInput in) throws RestApiException {
+ try {
+ return applyPatch.apply(change, in).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot apply patch", e);
+ }
+ }
+
+ @Override
public SubmittedTogetherInfo submittedTogether(
EnumSet<ListChangesOption> listOptions, EnumSet<SubmittedTogetherOption> submitOptions)
throws RestApiException {
@@ -583,6 +597,7 @@
}
}
+ @Nullable
@Override
public AccountInfo getAssignee() throws RestApiException {
try {
@@ -602,6 +617,7 @@
}
}
+ @Nullable
@Override
public AccountInfo deleteAssignee() throws RestApiException {
try {
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index 8014e17..bd31356f 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -36,6 +36,7 @@
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
@@ -724,6 +725,7 @@
return filterApprovals(byPatchSet(notes, psId), accountId);
}
+ @Nullable
public PatchSetApproval getSubmitter(ChangeNotes notes, PatchSet.Id c) {
if (c == null) {
return null;
@@ -736,6 +738,7 @@
}
}
+ @Nullable
public static PatchSetApproval getSubmitter(PatchSet.Id c, Iterable<PatchSetApproval> approvals) {
if (c == null) {
return null;
diff --git a/java/com/google/gerrit/server/cache/CacheInfo.java b/java/com/google/gerrit/server/cache/CacheInfo.java
index d6eb065..94a9e05 100644
--- a/java/com/google/gerrit/server/cache/CacheInfo.java
+++ b/java/com/google/gerrit/server/cache/CacheInfo.java
@@ -16,6 +16,7 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheStats;
+import com.google.gerrit.common.Nullable;
public class CacheInfo {
@@ -53,6 +54,7 @@
}
}
+ @Nullable
private static String duration(double ns) {
if (ns < 0.5) {
return null;
@@ -118,6 +120,7 @@
disk = percent(value, total);
}
+ @Nullable
private static Integer percent(long value, long total) {
if (total <= 0) {
return null;
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java b/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
index ec527ba..e9b254b 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
@@ -18,6 +18,7 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import java.io.IOException;
@@ -80,6 +81,7 @@
return !diskEnabled || diskLimit <= 0;
}
+ @Nullable
private static Path getCacheDir(SitePaths site, String name) {
if (name == null) {
return null;
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index aa62745..b744058 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -47,6 +47,7 @@
return source.refreshAfterWrite();
}
+ @Nullable
@Override
public Weigher<K, V> weigher() {
Weigher<K, V> weigher = source.weigher();
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 0403408..8327b88 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -103,6 +103,7 @@
this.mem = mem;
}
+ @Nullable
@Override
public V getIfPresent(Object objKey) {
if (!keyType.getRawType().isInstance(objKey)) {
@@ -423,6 +424,7 @@
return b == null || b.mightContain(key);
}
+ @Nullable
private BloomFilter<K> buildBloomFilter() {
SqlHandle c = null;
try {
@@ -472,6 +474,7 @@
}
}
+ @Nullable
ValueHolder<V> getIfPresent(K key) {
SqlHandle c = null;
try {
@@ -717,6 +720,7 @@
}
}
+ @Nullable
private SqlHandle close(SqlHandle h) {
if (h != null) {
h.close();
@@ -776,6 +780,7 @@
}
}
+ @Nullable
private PreparedStatement closeStatement(PreparedStatement ps) {
if (ps != null) {
try {
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 63e2c08..ed6d53d 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -106,6 +106,7 @@
to.actions = toActionMap(rsrc, visitors, changeInfo, copy(visitors, to));
}
+ @Nullable
private ChangeInfo copy(List<ActionVisitor> visitors, ChangeInfo changeInfo) {
if (visitors.isEmpty()) {
return null;
@@ -152,6 +153,7 @@
return copy;
}
+ @Nullable
private RevisionInfo copy(List<ActionVisitor> visitors, RevisionInfo revisionInfo) {
if (visitors.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index edaca70..d575324 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -29,6 +29,8 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
@@ -242,32 +244,38 @@
return change;
}
+ @CanIgnoreReturnValue
public ChangeInserter setTopic(String topic) {
checkState(change == null, "setTopic(String) only valid before creating change");
this.topic = topic;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setCherryPickOf(PatchSet.Id cherryPickOf) {
this.cherryPickOf = cherryPickOf;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setMessage(String message) {
this.message = message;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setPatchSetDescription(String patchSetDescription) {
this.patchSetDescription = patchSetDescription;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setValidate(boolean validate) {
this.validate = validate;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setReviewersAndCcs(
Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
return setReviewersAndCcsAsStrings(
@@ -275,35 +283,57 @@
Iterables.transform(ccs, Account.Id::toString));
}
+ @CanIgnoreReturnValue
+ public ChangeInserter setReviewersAndCcsIgnoreVisibility(
+ Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
+ return setReviewersAndCcsAsStrings(
+ Iterables.transform(reviewers, Account.Id::toString),
+ Iterables.transform(ccs, Account.Id::toString),
+ /* skipVisibilityCheck= */ true);
+ }
+
+ @CanIgnoreReturnValue
public ChangeInserter setReviewersAndCcsAsStrings(
Iterable<String> reviewers, Iterable<String> ccs) {
+ return setReviewersAndCcsAsStrings(reviewers, ccs, /* skipVisibilityCheck= */ false);
+ }
+
+ @CanIgnoreReturnValue
+ private ChangeInserter setReviewersAndCcsAsStrings(
+ Iterable<String> reviewers, Iterable<String> ccs, boolean skipVisibilityCheck) {
reviewerInputs =
Streams.concat(
Streams.stream(reviewers)
.distinct()
- .map(id -> newReviewerInput(id, ReviewerState.REVIEWER)),
- Streams.stream(ccs).distinct().map(id -> newReviewerInput(id, ReviewerState.CC)))
+ .map(id -> newReviewerInput(id, ReviewerState.REVIEWER, skipVisibilityCheck)),
+ Streams.stream(ccs)
+ .distinct()
+ .map(id -> newReviewerInput(id, ReviewerState.CC, skipVisibilityCheck)))
.collect(toImmutableList());
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setPrivate(boolean isPrivate) {
checkState(change == null, "setPrivate(boolean) only valid before creating change");
this.isPrivate = isPrivate;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setWorkInProgress(boolean workInProgress) {
this.workInProgress = workInProgress;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setStatus(Change.Status status) {
checkState(change == null, "setStatus(Change.Status) only valid before creating change");
this.status = status;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setGroups(List<String> groups) {
requireNonNull(groups, "groups may not be empty");
checkState(patchSet == null, "setGroups(List<String>) only valid before creating change");
@@ -311,6 +341,7 @@
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setValidationOptions(
ImmutableListMultimap<String, String> validationOptions) {
requireNonNull(validationOptions, "validationOptions may not be null");
@@ -322,21 +353,25 @@
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setFireRevisionCreated(boolean fireRevisionCreated) {
this.fireRevisionCreated = fireRevisionCreated;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setSendMail(boolean sendMail) {
this.sendMail = sendMail;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setRequestScopePropagator(RequestScopePropagator r) {
this.requestScopePropagator = r;
return this;
}
+ @CanIgnoreReturnValue
public ChangeInserter setRevertOf(Change.Id revertOf) {
this.revertOf = revertOf;
return this;
@@ -351,6 +386,7 @@
return patchSet;
}
+ @CanIgnoreReturnValue
public ChangeInserter setApprovals(Map<String, Short> approvals) {
this.approvals = approvals;
return this;
@@ -368,11 +404,13 @@
* @param updateRef whether to update the ref during {@link #updateRepo(RepoContext)}.
*/
@Deprecated
+ @CanIgnoreReturnValue
public ChangeInserter setUpdateRef(boolean updateRef) {
this.updateRef = updateRef;
return this;
}
+ @Nullable
public String getChangeMessage() {
if (message == null) {
return null;
@@ -595,7 +633,8 @@
}
}
- private static InternalReviewerInput newReviewerInput(String reviewer, ReviewerState state) {
+ private static InternalReviewerInput newReviewerInput(
+ String reviewer, ReviewerState state, boolean skipVisibilityCheck) {
// Disable individual emails when adding reviewers, as all reviewers will receive the single
// bulk new change email.
InternalReviewerInput input =
@@ -606,7 +645,9 @@
// certain commit footers: putting a nonexistent user in a footer should not cause an error. In
// theory we could provide finer control to do this for some reviewers and not others, but it's
// not worth complicating the ChangeInserter interface further at this time.
- input.otherFailureBehavior = ReviewerModifier.FailureBehavior.IGNORE;
+ input.otherFailureBehavior = ReviewerModifier.FailureBehavior.IGNORE_EXCEPT_NOT_FOUND;
+
+ input.skipVisibilityCheck = skipVisibilityCheck;
return input;
}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 02b0a60..500bb77 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -45,6 +45,7 @@
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -931,11 +932,12 @@
}
src = Collections.singletonList(ps);
}
- Map<PatchSet.Id, PatchSet> map = Maps.newHashMapWithExpectedSize(src.size());
+ // Sort by patch set ID in increasing order to have a stable output.
+ ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> map = ImmutableSortedMap.naturalOrder();
for (PatchSet patchSet : src) {
map.put(patchSet.id(), patchSet);
}
- return map;
+ return map.build();
}
private List<PluginDefinedInfo> getPluginInfos(ChangeData cd) {
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 0775647..38efc44 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -764,6 +764,7 @@
return serverIdent.get();
}
+ @Nullable
private RevCommit parseCommit(ObjectId objId, String desc) {
try {
return rw.parseCommit(objId);
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
index 44b4ded..d9c30d7 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
@@ -42,6 +42,7 @@
this.diffs = diffOperations;
}
+ @Nullable
@Override
public Map<String, FileInfo> getFileInfoMap(
Change change, ObjectId objectId, @Nullable PatchSet base)
@@ -63,6 +64,7 @@
}
}
+ @Nullable
@Override
public Map<String, FileInfo> getFileInfoMap(
Project.NameKey project, ObjectId objectId, int parent)
@@ -102,6 +104,14 @@
fileInfo.oldPath = FilePathAdapter.getOldPath(fileDiff.oldPath(), fileDiff.changeType());
fileInfo.sizeDelta = fileDiff.sizeDelta();
fileInfo.size = fileDiff.size();
+ fileInfo.oldMode =
+ fileDiff.oldMode().isPresent() && !fileDiff.oldMode().get().equals(Patch.FileMode.MISSING)
+ ? fileDiff.oldMode().get().getMode()
+ : null;
+ fileInfo.newMode =
+ fileDiff.newMode().isPresent() && !fileDiff.newMode().get().equals(Patch.FileMode.MISSING)
+ ? fileDiff.newMode().get().getMode()
+ : null;
if (fileDiff.patchType().get() == Patch.PatchType.BINARY) {
fileInfo.binary = true;
} else {
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 69a84dd8..cfa15ae 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -80,6 +80,7 @@
* lazily populate accounts. Callers have to call {@link AccountLoader#fill()} afterwards to
* populate all accounts in the returned {@link LabelInfo}s.
*/
+ @Nullable
Map<String, LabelInfo> labelsFor(
AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 2d36df2..ba938ee 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -17,6 +17,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -71,6 +72,7 @@
@AutoValue
public abstract static class Base {
+ @Nullable
private static Base create(ChangeNotes notes, PatchSet ps) {
if (notes == null) {
return null;
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index 9580565..f3ad4f7 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -94,9 +94,21 @@
public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
public static final int DEFAULT_MAX_REVIEWERS = 20;
+ /**
+ * Controls which failures should be ignored.
+ *
+ * <p>If a failure is ignored the operation succeeds, but the reviewer is not added. If not
+ * ignored a failure means that the operation fails.
+ */
public enum FailureBehavior {
+ // All failures cause the operation to fail.
FAIL,
- IGNORE;
+
+ // Only not found failures cause the operation to fail, all other failures are ignored.
+ IGNORE_EXCEPT_NOT_FOUND,
+
+ // All failures are ignored.
+ IGNORE_ALL;
}
private enum FailureType {
@@ -113,6 +125,9 @@
* resolving to an account/group/email.
*/
public FailureBehavior otherFailureBehavior = FailureBehavior.FAIL;
+
+ /** Whether the visibility check for the reviewer account should be skipped. */
+ public boolean skipVisibilityCheck = false;
}
public static InternalReviewerInput newReviewerInput(
@@ -143,7 +158,7 @@
in.reviewer = accountId.toString();
in.state = CC;
in.notify = notify;
- in.otherFailureBehavior = FailureBehavior.IGNORE;
+ in.otherFailureBehavior = FailureBehavior.IGNORE_ALL;
return Optional.of(in);
}
@@ -262,7 +277,13 @@
IdentifiedUser reviewerUser;
boolean exactMatchFound = false;
try {
- reviewerUser = accountResolver.resolveIncludeInactive(input.reviewer).asUniqueUser();
+ if (input instanceof InternalReviewerInput
+ && ((InternalReviewerInput) input).skipVisibilityCheck) {
+ reviewerUser =
+ accountResolver.resolveIncludeInactiveIgnoreVisibility(input.reviewer).asUniqueUser();
+ } else {
+ reviewerUser = accountResolver.resolveIncludeInactive(input.reviewer).asUniqueUser();
+ }
if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
|| input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
exactMatchFound = true;
@@ -577,7 +598,9 @@
(input instanceof InternalReviewerInput)
? ((InternalReviewerInput) input).otherFailureBehavior
: FailureBehavior.FAIL;
- return failureType == FailureType.OTHER && behavior == FailureBehavior.IGNORE;
+ return behavior == FailureBehavior.IGNORE_ALL
+ || (failureType == FailureType.OTHER
+ && behavior == FailureBehavior.IGNORE_EXCEPT_NOT_FOUND);
}
}
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 496808a..d581675 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -16,6 +16,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.CoreDownloadSchemes;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
import com.google.gerrit.server.change.ArchiveFormatInternal;
@@ -87,6 +88,7 @@
return list.size() == 1 && list.get(0) == null;
}
+ @Nullable
private static String toCoreScheme(String s) {
try {
Field f = CoreDownloadSchemes.class.getField(s.toUpperCase());
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index e5b063b..f442500 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -201,6 +201,7 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
import com.google.gerrit.server.query.change.DistinctVotersPredicate;
+import com.google.gerrit.server.query.change.HasSubmoduleUpdatePredicate;
import com.google.gerrit.server.quota.QuotaEnforcer;
import com.google.gerrit.server.restapi.change.OnPostReview;
import com.google.gerrit.server.restapi.change.SuggestReviewers;
@@ -299,6 +300,7 @@
factory(ChangeJson.AssistedFactory.class);
factory(ChangeIsVisibleToPredicate.Factory.class);
factory(DistinctVotersPredicate.Factory.class);
+ factory(HasSubmoduleUpdatePredicate.Factory.class);
factory(DeadlineChecker.Factory.class);
factory(EmailNewPatchSet.Factory.class);
factory(MultiProgressMonitor.Factory.class);
diff --git a/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java b/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
index 2a74833..f3f7645 100644
--- a/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
+++ b/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
@@ -14,24 +14,24 @@
package com.google.gerrit.server.config;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
-public class GerritImportedServerIdsProvider implements Provider<ImmutableList<String>> {
+public class GerritImportedServerIdsProvider implements Provider<ImmutableSet<String>> {
public static final String SECTION = "gerrit";
public static final String KEY = "importedServerId";
- private final ImmutableList<String> importedIds;
+ private final ImmutableSet<String> importedIds;
@Inject
public GerritImportedServerIdsProvider(@GerritServerConfig Config cfg) {
- importedIds = ImmutableList.copyOf(cfg.getStringList(SECTION, null, KEY));
+ importedIds = ImmutableSet.copyOf(cfg.getStringList(SECTION, null, KEY));
}
@Override
- public ImmutableList<String> get() {
+ public ImmutableSet<String> get() {
return importedIds;
}
}
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index 99bd62d..0a213b4 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -99,6 +99,7 @@
return values.length > 0 && isNullOrEmpty(values[0]);
}
+ @Nullable
private static GitwebType typeFromConfig(Config cfg) {
GitwebType defaultType = defaultType(cfg.getString("gitweb", null, "type"));
if (defaultType == null) {
@@ -136,6 +137,7 @@
return type;
}
+ @Nullable
private static GitwebType defaultType(String typeName) {
GitwebType type = new GitwebType();
switch (nullToEmpty(typeName)) {
@@ -283,6 +285,7 @@
this.tag = parse(type.getTag());
}
+ @Nullable
@Override
public WebLinkInfo getBranchWebLink(String projectName, String branchName) {
if (branch != null) {
@@ -295,6 +298,7 @@
return null;
}
+ @Nullable
@Override
public WebLinkInfo getTagWebLink(String projectName, String tagName) {
if (tag != null) {
@@ -304,6 +308,7 @@
return null;
}
+ @Nullable
@Override
public WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName) {
if (fileHistory != null) {
@@ -317,6 +322,7 @@
return null;
}
+ @Nullable
@Override
public WebLinkInfo getFileWebLink(
String projectName, String revision, String hash, String fileName) {
@@ -331,6 +337,7 @@
return null;
}
+ @Nullable
@Override
public WebLinkInfo getPatchSetWebLink(
String projectName, String commit, String commitMessage, String branchName) {
@@ -359,6 +366,7 @@
return getPatchSetWebLink(projectName, commit, commitMessage, branchName);
}
+ @Nullable
@Override
public WebLinkInfo getProjectWeblink(String projectName) {
if (project != null) {
@@ -378,6 +386,7 @@
return new WebLinkInfo(type.getLinkName(), null, url + rest, null);
}
+ @Nullable
private static ParameterizedString parse(String pattern) {
if (!isNullOrEmpty(pattern)) {
return new ParameterizedString(pattern);
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index c09988e3..e11d6aa 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -17,6 +17,7 @@
import static java.util.stream.Collectors.toList;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
@@ -360,6 +361,7 @@
}
}
+ @Nullable
private ProjectConfig parseConfig(Project.NameKey p, String idStr)
throws IOException, ConfigInvalidException, RepositoryNotFoundException {
ObjectId id = ObjectId.fromString(idStr);
@@ -382,14 +384,17 @@
}
}
+ @Nullable
private static Boolean toBoolean(String value) {
return value != null ? Boolean.parseBoolean(value) : null;
}
+ @Nullable
private static Integer toInt(String value) {
return value != null ? Integer.parseInt(value) : null;
}
+ @Nullable
private static Long toLong(String value) {
return value != null ? Long.parseLong(value) : null;
}
diff --git a/java/com/google/gerrit/server/config/RepositoryConfig.java b/java/com/google/gerrit/server/config/RepositoryConfig.java
index f722321..d569c87 100644
--- a/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -55,6 +55,7 @@
cfg.getStringList(SECTION_NAME, findSubSection(project.get()), OWNER_GROUP_NAME));
}
+ @Nullable
public Path getBasePath(Project.NameKey project) {
String basePath = cfg.getString(SECTION_NAME, findSubSection(project.get()), BASE_PATH_NAME);
return basePath != null ? Paths.get(basePath) : null;
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 5e268da..2cd24ba 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.config;
import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -140,6 +141,7 @@
* @param path the path string to resolve. May be null.
* @return the resolved path; null if {@code path} was null or empty.
*/
+ @Nullable
public Path resolve(String path) {
if (path != null && !path.isEmpty()) {
Path loc = site_path.resolve(path).normalize();
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index c2c0b05..0e911b9 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -21,6 +21,7 @@
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.vladsch.flexmark.ast.Heading;
import com.vladsch.flexmark.ast.util.TextCollectingVisitor;
import com.vladsch.flexmark.html.HtmlRenderer;
@@ -126,6 +127,7 @@
return findTitle(parseMarkdown(md));
}
+ @Nullable
private String findTitle(Node root) {
if (root instanceof Heading) {
Heading h = (Heading) root;
diff --git a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index 7d3ddf1..cd49ea6 100644
--- a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -16,6 +16,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -99,6 +100,7 @@
}
}
+ @Nullable
protected Directory readIndexDirectory() throws IOException {
Directory dir = new ByteBuffersDirectory();
byte[] buffer = new byte[4096];
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 2957d6b..903a4c0 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.base.Charsets;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -226,13 +227,18 @@
* @param notes the {@link ChangeNotes} of the change whose change edit should be modified
* @param filePath the path of the file whose contents should be modified
* @param newContent the new file content
+ * @param newGitFileMode the new file mode in octal format. {@code null} indicates no change
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws BadRequestException if the user provided bad input (e.g. invalid file paths)
* @throws InvalidChangeOperationException if the file already had the specified content
* @throws ResourceConflictException if the project state does not permit the operation
*/
public void modifyFile(
- Repository repository, ChangeNotes notes, String filePath, RawInput newContent)
+ Repository repository,
+ ChangeNotes notes,
+ String filePath,
+ RawInput newContent,
+ @Nullable Integer newGitFileMode)
throws AuthException, BadRequestException, InvalidChangeOperationException, IOException,
PermissionBackendException, ResourceConflictException {
modifyCommit(
@@ -240,7 +246,8 @@
notes,
new ModificationIntention.LatestCommit(),
CommitModification.builder()
- .addTreeModification(new ChangeFileContentModification(filePath, newContent))
+ .addTreeModification(
+ new ChangeFileContentModification(filePath, newContent, newGitFileMode))
.build());
}
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index 9c0b92a..96c6685 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RawInput;
import java.io.IOException;
import java.io.InputStream;
@@ -42,16 +43,26 @@
private final String filePath;
private final RawInput newContent;
+ private final Integer newGitFileMode;
public ChangeFileContentModification(String filePath, RawInput newContent) {
this.filePath = filePath;
this.newContent = requireNonNull(newContent, "new content required");
+ this.newGitFileMode = null;
+ }
+
+ public ChangeFileContentModification(
+ String filePath, RawInput newContent, @Nullable Integer newGitFileMode) {
+ this.filePath = filePath;
+ this.newContent = requireNonNull(newContent, "new content required");
+ this.newGitFileMode = newGitFileMode;
}
@Override
public List<DirCacheEditor.PathEdit> getPathEdits(
Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents) {
- DirCacheEditor.PathEdit changeContentEdit = new ChangeContent(filePath, newContent, repository);
+ DirCacheEditor.PathEdit changeContentEdit =
+ new ChangeContent(filePath, newContent, repository, newGitFileMode);
return Collections.singletonList(changeContentEdit);
}
@@ -70,16 +81,32 @@
private final RawInput newContent;
private final Repository repository;
+ private final Integer newGitFileMode;
- ChangeContent(String filePath, RawInput newContent, Repository repository) {
+ ChangeContent(
+ String filePath,
+ RawInput newContent,
+ Repository repository,
+ @Nullable Integer newGitFileMode) {
super(filePath);
this.newContent = newContent;
this.repository = repository;
+ this.newGitFileMode = newGitFileMode;
+ }
+
+ private boolean isValidGitFileMode(int gitFileMode) {
+ return (gitFileMode == 100755) || (gitFileMode == 100644);
}
@Override
public void apply(DirCacheEntry dirCacheEntry) {
try {
+ if (newGitFileMode != null && newGitFileMode != 0) {
+ if (!isValidGitFileMode(newGitFileMode)) {
+ throw new IllegalStateException("GitFileMode " + newGitFileMode + " is invalid");
+ }
+ dirCacheEntry.setFileMode(FileMode.fromBits(newGitFileMode));
+ }
if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
dirCacheEntry.setLength(0);
dirCacheEntry.setLastModified(Instant.EPOCH);
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 95f6d96..60e30bc 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
@@ -523,6 +524,7 @@
}
/** Create an AuthorAttribute for the given account suitable for serialization to JSON. */
+ @Nullable
public AccountAttribute asAccountAttribute(Account.Id id) {
if (id == null) {
return null;
@@ -590,6 +592,7 @@
}
/** Get a link to the change; null if the server doesn't know its own address. */
+ @Nullable
private String getChangeUrl(Change change) {
if (change != null) {
return urlFormatter.get().getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index afe2a7c..18f3d7a 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -20,6 +20,7 @@
import com.google.common.base.Suppliers;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
@@ -234,6 +235,7 @@
});
}
+ @Nullable
String[] hashtagArray(Collection<String> hashtags) {
if (hashtags != null && !hashtags.isEmpty()) {
return Sets.newHashSet(hashtags).toArray(new String[hashtags.size()]);
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index b876341..efc8d54 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -27,6 +27,9 @@
public static String GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG =
"GerritBackendRequestFeature__remove_revision_etag";
+ public static String GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION =
+ "GerritBackendFeature__attach_nonce_to_documentation";
+
/** Features, enabled by default in the current release. */
public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
ImmutableSet.of(UI_FEATURE_PATCHSET_COMMENTS, UI_FEATURE_SUBMIT_REQUIREMENTS_UI);
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index b669571..7c8777f 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -99,6 +99,7 @@
return revisionJsonFactory.create(changeOptions).getRevisionInfo(cd, ps);
}
+ @Nullable
public AccountInfo accountInfo(@Nullable AccountState accountState) {
if (accountState == null || accountState.account().id() == null) {
return null;
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index fa46bf4..2841f92 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -26,10 +26,13 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommonConverters;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
@@ -44,6 +47,8 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -58,15 +63,19 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
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;
@@ -85,6 +94,7 @@
private final NotifyResolver notifyResolver;
private final RevertedSender.Factory revertedSenderFactory;
private final ChangeMessagesUtil cmUtil;
+ private final ChangeNotes.Factory changeNotesFactory;
private final ChangeReverted changeReverted;
private final BatchUpdate.Factory updateFactory;
private final MessageIdGenerator messageIdGenerator;
@@ -99,6 +109,7 @@
NotifyResolver notifyResolver,
RevertedSender.Factory revertedSenderFactory,
ChangeMessagesUtil cmUtil,
+ ChangeNotes.Factory changeNotesFactory,
ChangeReverted changeReverted,
BatchUpdate.Factory updateFactory,
MessageIdGenerator messageIdGenerator) {
@@ -110,6 +121,7 @@
this.notifyResolver = notifyResolver;
this.revertedSenderFactory = revertedSenderFactory;
this.cmUtil = cmUtil;
+ this.changeNotesFactory = changeNotesFactory;
this.changeReverted = changeReverted;
this.updateFactory = updateFactory;
this.messageIdGenerator = messageIdGenerator;
@@ -190,6 +202,41 @@
}
/**
+ * Creates a commit with the specified tree ID.
+ *
+ * @param oi ObjectInserter for inserting the newly created commit.
+ * @param authorIdent of the new commit
+ * @param committerIdent of the new commit
+ * @param parentCommit of the new commit. Can be null.
+ * @param commitMessage for the new commit.
+ * @param treeId of the content for the new commit.
+ * @return the newly created commit.
+ * @throws IOException if fails to insert the commit.
+ */
+ public static ObjectId createCommitWithTree(
+ ObjectInserter oi,
+ PersonIdent authorIdent,
+ PersonIdent committerIdent,
+ @Nullable RevCommit parentCommit,
+ String commitMessage,
+ ObjectId treeId)
+ throws IOException {
+ logger.atFine().log("Creating commit with tree: %s", treeId.getName());
+ CommitBuilder commit = new CommitBuilder();
+ commit.setTreeId(treeId);
+ if (parentCommit != null) {
+ commit.setParentId(parentCommit);
+ }
+ commit.setAuthor(authorIdent);
+ commit.setCommitter(committerIdent);
+ commit.setMessage(commitMessage);
+
+ ObjectId id = oi.insert(commit);
+ oi.flush();
+ return id;
+ }
+
+ /**
* Creates a revert commit.
*
* @param message Commit message for the revert commit.
@@ -227,12 +274,6 @@
RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
revWalk.parseHeaders(parentToCommitToRevert);
- CommitBuilder revertCommitBuilder = new CommitBuilder();
- revertCommitBuilder.addParentId(commitToRevert);
- revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
- revertCommitBuilder.setAuthor(authorIdent);
- revertCommitBuilder.setCommitter(authorIdent);
-
Change changeToRevert = notes.getChange();
String subject = changeToRevert.getSubject();
if (subject.length() > 63) {
@@ -244,11 +285,11 @@
ChangeMessages.get().revertChangeDefaultMessage, subject, patch.commitId().name());
}
if (generatedChangeId != null) {
- revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, generatedChangeId, true));
+ message = ChangeIdUtil.insertId(message, generatedChangeId, true);
}
- ObjectId id = oi.insert(revertCommitBuilder);
- oi.flush();
- return id;
+
+ return createCommitWithTree(
+ oi, authorIdent, committerIdent, commitToRevert, message, parentToCommitToRevert.getTree());
}
private Change.Id createRevertChangeFromCommit(
@@ -263,17 +304,17 @@
Repository git)
throws IOException, RestApiException, UpdateException, ConfigInvalidException {
RevCommit revertCommit = revWalk.parseCommit(revertCommitId);
- Change changeToRevert = notes.getChange();
Change.Id changeId = Change.id(seq.nextChangeId());
if (input.workInProgress) {
- input.notify = firstNonNull(input.notify, NotifyHandling.OWNER);
+ input.notify = firstNonNull(input.notify, NotifyHandling.NONE);
}
NotifyResolver.Result notify =
notifyResolver.resolve(firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
+ Change changeToRevert = notes.getChange();
ChangeInserter ins =
changeInserterFactory
- .create(changeId, revertCommit, notes.getChange().getDest().branch())
+ .create(changeId, revertCommit, changeToRevert.getDest().branch())
.setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
ins.setMessage("Uploaded patch set 1.");
ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
@@ -286,7 +327,7 @@
reviewers.remove(user.getAccountId());
Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
ccs.remove(user.getAccountId());
- ins.setReviewersAndCcs(reviewers, ccs);
+ ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
ins.setRevertOf(notes.getChangeId());
ins.setWorkInProgress(input.workInProgress);
@@ -294,8 +335,10 @@
bu.setRepository(git, revWalk, oi);
bu.setNotify(notify);
bu.insertChange(ins);
- bu.addOp(changeId, new NotifyOp(changeToRevert, ins));
- bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(generatedChangeId));
+ if (!input.workInProgress) {
+ addChangeRevertedNotificationOps(
+ bu, changeToRevert.getId(), changeId, generatedChangeId.name());
+ }
bu.execute();
}
return changeId;
@@ -315,47 +358,140 @@
return validationOptionsBuilder.build();
}
- private class NotifyOp implements BatchUpdateOp {
- private final Change change;
- private final ChangeInserter ins;
+ /**
+ * Notify the owners of a change that their change is being reverted.
+ *
+ * @param bu to append the notification actions to.
+ * @param revertedChangeId to be notified.
+ * @param revertingChangeId to notify about.
+ * @param revertingChangeKey to notify about.
+ */
+ public void addChangeRevertedNotificationOps(
+ BatchUpdate bu,
+ Change.Id revertedChangeId,
+ Change.Id revertingChangeId,
+ String revertingChangeKey) {
+ bu.addOp(revertingChangeId, new ChangeRevertedNotifyOp(revertedChangeId, revertingChangeId));
+ bu.addOp(revertedChangeId, new PostRevertedMessageOp(revertingChangeKey));
+ }
- NotifyOp(Change change, ChangeInserter ins) {
- this.change = change;
- this.ins = ins;
+ private class ChangeRevertedNotifyOp implements BatchUpdateOp {
+ private final Change.Id revertedChangeId;
+ private final Change.Id revertingChangeId;
+
+ ChangeRevertedNotifyOp(Change.Id revertedChangeId, Change.Id revertingChangeId) {
+ this.revertedChangeId = revertedChangeId;
+ this.revertingChangeId = revertingChangeId;
}
@Override
public void postUpdate(PostUpdateContext ctx) throws Exception {
- changeReverted.fire(
- ctx.getChangeData(change), ctx.getChangeData(ins.getChange()), ctx.getWhen());
+ ChangeData revertedChange =
+ ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertedChangeId));
+ ChangeData revertingChange =
+ ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertingChangeId));
+ changeReverted.fire(revertedChange, revertingChange, ctx.getWhen());
try {
- RevertedSender emailSender = revertedSenderFactory.create(ctx.getProject(), change.getId());
+ RevertedSender emailSender =
+ revertedSenderFactory.create(ctx.getProject(), revertedChange.getId());
emailSender.setFrom(ctx.getAccountId());
- emailSender.setNotify(ctx.getNotify(change.getId()));
+ emailSender.setNotify(ctx.getNotify(revertedChangeId));
emailSender.setMessageId(
- messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
+ messageIdGenerator.fromChangeUpdate(
+ ctx.getRepoView(), revertedChange.currentPatchSet().id()));
emailSender.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
- "Cannot send email for revert change %s", change.getId());
+ "Cannot send email for revert change %s", revertedChangeId);
}
}
}
private class PostRevertedMessageOp implements BatchUpdateOp {
- private final ObjectId computedChangeId;
+ private final String revertingChangeKey;
- PostRevertedMessageOp(ObjectId computedChangeId) {
- this.computedChangeId = computedChangeId;
+ PostRevertedMessageOp(String revertingChangeKey) {
+ this.revertingChangeKey = revertingChangeKey;
}
@Override
public boolean updateChange(ChangeContext ctx) {
cmUtil.setChangeMessage(
ctx,
- "Created a revert of this change as I" + computedChangeId.name(),
+ "Created a revert of this change as I" + revertingChangeKey,
ChangeMessagesUtil.TAG_REVERT);
return true;
}
}
+
+ /**
+ * Returns the parent commit for a new commit.
+ *
+ * <p>If {@code baseSha1} is provided, the method verifies it can be used as a base. If {@code
+ * baseSha1} is not provided the tip of the {@code destRef} is returned.
+ *
+ * @param project The name of the project.
+ * @param changeQuery Used for looking up the base commit.
+ * @param revWalk Used for parsing the base commit.
+ * @param destRef The destination branch.
+ * @param baseSha1 The hash of the base commit. Nullable.
+ * @return the base commit. Either the commit matching the provided hash, or the direct parent if
+ * a hash was not provided.
+ * @throws IOException if the branch reference cannot be parsed.
+ * @throws RestApiException if the base commit cannot be fetched.
+ */
+ public static RevCommit getBaseCommit(
+ String project,
+ InternalChangeQuery changeQuery,
+ RevWalk revWalk,
+ Ref destRef,
+ @Nullable String baseSha1)
+ throws IOException, RestApiException {
+ RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId());
+ // The tip commit of the destination ref is the default base for the newly created change.
+ if (Strings.isNullOrEmpty(baseSha1)) {
+ return destRefTip;
+ }
+
+ ObjectId baseObjectId;
+ try {
+ baseObjectId = ObjectId.fromString(baseSha1);
+ } catch (InvalidObjectIdException e) {
+ throw new BadRequestException(
+ String.format("Base %s doesn't represent a valid SHA-1", baseSha1), e);
+ }
+
+ RevCommit baseCommit;
+ try {
+ baseCommit = revWalk.parseCommit(baseObjectId);
+ } catch (MissingObjectException e) {
+ throw new UnprocessableEntityException(
+ String.format("Base %s doesn't exist", baseObjectId.name()), e);
+ }
+
+ changeQuery.enforceVisibility(true);
+ List<ChangeData> changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), baseSha1);
+
+ if (changeDatas.isEmpty()) {
+ if (revWalk.isMergedInto(baseCommit, destRefTip)) {
+ // The base commit is a merged commit with no change associated.
+ return baseCommit;
+ }
+ throw new UnprocessableEntityException(
+ String.format("Commit %s does not exist on branch %s", baseSha1, destRef.getName()));
+ } else if (changeDatas.size() != 1) {
+ throw new ResourceConflictException("Multiple changes found for commit " + baseSha1);
+ }
+
+ Change change = changeDatas.get(0).change();
+ if (!change.isAbandoned()) {
+ // The base commit is a valid change revision.
+ return baseCommit;
+ }
+
+ throw new ResourceConflictException(
+ String.format(
+ "Change %s with commit %s is %s",
+ change.getChangeId(), baseSha1, ChangeUtil.status(change)));
+ }
}
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index 5bbe5e2..455b221 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -27,6 +27,7 @@
import com.google.common.collect.Sets;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.PatchSetUtil;
@@ -258,6 +259,7 @@
return actual;
}
+ @Nullable
private ObjectId parseGroup(ObjectId forCommit, String group) {
try {
return ObjectId.fromString(group);
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index ae247ad..6922efb 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -1027,6 +1027,7 @@
}
}
+ @Nullable
public static CodeReviewCommit findAnyMergedInto(
CodeReviewRevWalk rw, Iterable<CodeReviewCommit> commits, CodeReviewCommit tip)
throws IOException {
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 290e1e7..c76c78e 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -16,8 +16,10 @@
import static com.google.gerrit.server.DeadlineChecker.getTimeoutFormatter;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.base.Ticker;
import com.google.common.flogger.FluentLogger;
@@ -592,9 +594,12 @@
}
private void send(StringBuilder s) {
+ String progress = s.toString();
+ logger.atInfo().atMostEvery(1, MINUTES).log(
+ "%s", CharMatcher.javaIsoControl().removeFrom(progress));
if (!clientDisconnected) {
try {
- out.write(Constants.encode(s.toString()));
+ out.write(Constants.encode(progress));
out.flush();
} catch (IOException e) {
logger.atWarning().withCause(e).log(
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index f66a089..fb34753 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -82,6 +82,7 @@
throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
}
+ @Nullable
@Override
public Ref exactRef(String name) throws IOException {
Ref ref = getDelegate().getRefDatabase().exactRef(name);
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index e2f9abd..727aab3 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -41,9 +41,9 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import com.google.inject.util.Providers;
-import java.util.ArrayList;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
@@ -162,14 +162,20 @@
List<ChangeData> cds =
queryProvider
.get()
- .setRequestedFields(ChangeField.CHANGE, ChangeField.REVIEWER)
+ .setRequestedFields(ChangeField.CHANGE, ChangeField.REVIEWER_SPEC)
.byProject(key);
- List<CachedChange> result = new ArrayList<>(cds.size());
+ Map<Change.Id, CachedChange> result = new HashMap<>(cds.size());
for (ChangeData cd : cds) {
- result.add(
+ if (result.containsKey(cd.getId())) {
+ logger.atWarning().log(
+ "Duplicate changes returned from change query by project %s: %s, %s",
+ key, cd.change(), result.get(cd.getId()).change());
+ }
+ result.put(
+ cd.getId(),
new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.reviewers()));
}
- return Collections.unmodifiableList(result);
+ return List.copyOf(result.values());
}
}
}
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 3032bfe..e8b7c62 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -18,8 +18,10 @@
import com.google.common.base.CaseFormat;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
@@ -27,6 +29,7 @@
import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import com.google.gerrit.server.logging.LoggingContext;
import com.google.gerrit.server.logging.LoggingContextAwareRunnable;
+import com.google.gerrit.server.plugincontext.PluginMapContext;
import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -49,8 +52,8 @@
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jgit.lib.Config;
/** Delayed execution of tasks using a background thread pool. */
@@ -58,6 +61,30 @@
public class WorkQueue {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ /**
+ * To register a TaskListener, which will be called directly before Tasks run, and directly after
+ * they complete, bind the TaskListener like this:
+ *
+ * <p><code>
+ * bind(TaskListener.class)
+ * .annotatedWith(Exports.named("MyListener"))
+ * .to(MyListener.class);
+ * </code>
+ */
+ public interface TaskListener {
+ public static class NoOp implements TaskListener {
+ @Override
+ public void onStart(Task<?> task) {}
+
+ @Override
+ public void onStop(Task<?> task) {}
+ }
+
+ void onStart(Task<?> task);
+
+ void onStop(Task<?> task);
+ }
+
public static class Lifecycle implements LifecycleListener {
private final WorkQueue workQueue;
@@ -78,6 +105,7 @@
public static class WorkQueueModule extends LifecycleModule {
@Override
protected void configure() {
+ DynamicMap.mapOf(binder(), WorkQueue.TaskListener.class);
bind(WorkQueue.class);
listener().to(Lifecycle.class);
}
@@ -87,18 +115,32 @@
private final IdGenerator idGenerator;
private final MetricMaker metrics;
private final CopyOnWriteArrayList<Executor> queues;
+ private final PluginMapContext<TaskListener> listeners;
@Inject
- WorkQueue(IdGenerator idGenerator, @GerritServerConfig Config cfg, MetricMaker metrics) {
- this(idGenerator, Math.max(cfg.getInt("execution", "defaultThreadPoolSize", 2), 2), metrics);
+ WorkQueue(
+ IdGenerator idGenerator,
+ @GerritServerConfig Config cfg,
+ MetricMaker metrics,
+ PluginMapContext<TaskListener> listeners) {
+ this(
+ idGenerator,
+ Math.max(cfg.getInt("execution", "defaultThreadPoolSize", 2), 2),
+ metrics,
+ listeners);
}
/** Constructor to allow binding the WorkQueue more explicitly in a vhost setup. */
- public WorkQueue(IdGenerator idGenerator, int defaultThreadPoolSize, MetricMaker metrics) {
+ public WorkQueue(
+ IdGenerator idGenerator,
+ int defaultThreadPoolSize,
+ MetricMaker metrics,
+ PluginMapContext<TaskListener> listeners) {
this.idGenerator = idGenerator;
this.metrics = metrics;
this.queues = new CopyOnWriteArrayList<>();
this.defaultQueue = createQueue(defaultThreadPoolSize, "WorkQueue", true);
+ this.listeners = listeners;
}
/** Get the default work queue, for miscellaneous tasks. */
@@ -200,6 +242,7 @@
}
/** Locate a task by its unique id, null if no task matches. */
+ @Nullable
public Task<?> getTask(int id) {
Task<?> result = null;
for (Executor e : queues) {
@@ -215,6 +258,7 @@
return result;
}
+ @Nullable
public ScheduledThreadPoolExecutor getExecutor(String queueName) {
for (Executor e : queues) {
if (e.queueName.equals(queueName)) {
@@ -438,6 +482,14 @@
Collection<Task<?>> getTasks() {
return all.values();
}
+
+ public void onStart(Task<?> task) {
+ listeners.runEach(extension -> extension.getProvider().get().onStart(task));
+ }
+
+ public void onStop(Task<?> task) {
+ listeners.runEach(extension -> extension.getProvider().get().onStop(task));
+ }
}
private static void logUncaughtException(Thread t, Throwable e) {
@@ -474,18 +526,23 @@
* <ol>
* <li>{@link #SLEEPING}: if scheduled with a non-zero delay.
* <li>{@link #READY}: waiting for an available worker thread.
+ * <li>{@link #STARTING}: onStart() actively executing on a worker thread.
* <li>{@link #RUNNING}: actively executing on a worker thread.
+ * <li>{@link #STOPPING}: onStop() actively executing on a worker thread.
* <li>{@link #DONE}: finished executing, if not periodic.
* </ol>
*/
public enum State {
// Ordered like this so ordinal matches the order we would
// prefer to see tasks sorted in: done before running,
- // running before ready, ready before sleeping.
+ // stopping before running, running before starting,
+ // starting before ready, ready before sleeping.
//
DONE,
CANCELLED,
+ STOPPING,
RUNNING,
+ STARTING,
READY,
SLEEPING,
OTHER
@@ -495,15 +552,16 @@
private final RunnableScheduledFuture<V> task;
private final Executor executor;
private final int taskId;
- private final AtomicBoolean running;
private final Instant startTime;
+ // runningState is non-null when listener or task code is running in an executor thread
+ private final AtomicReference<State> runningState = new AtomicReference<>();
+
Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor, int taskId) {
this.runnable = runnable;
this.task = task;
this.executor = executor;
this.taskId = taskId;
- this.running = new AtomicBoolean();
this.startTime = Instant.now();
}
@@ -514,10 +572,13 @@
public State getState() {
if (isCancelled()) {
return State.CANCELLED;
+ }
+
+ State r = runningState.get();
+ if (r != null) {
+ return r;
} else if (isDone() && !isPeriodic()) {
return State.DONE;
- } else if (running.get()) {
- return State.RUNNING;
}
final long delay = getDelay(TimeUnit.MILLISECONDS);
@@ -538,14 +599,14 @@
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (task.cancel(mayInterruptIfRunning)) {
- // Tiny abuse of running: if the task needs to know it was
- // canceled (to clean up resources) and it hasn't started
+ // Tiny abuse of runningState: if the task needs to know it
+ // was canceled (to clean up resources) and it hasn't started
// yet the task's run method won't execute. So we tag it
// as running and allow it to clean up. This ensures we do
// not invoke cancel twice.
//
if (runnable instanceof CancelableRunnable) {
- if (running.compareAndSet(false, true)) {
+ if (runningState.compareAndSet(null, State.RUNNING)) {
((CancelableRunnable) runnable).cancel();
} else if (runnable instanceof CanceledWhileRunning) {
((CanceledWhileRunning) runnable).setCanceledWhileRunning();
@@ -605,16 +666,21 @@
@Override
public void run() {
- if (running.compareAndSet(false, true)) {
+ if (runningState.compareAndSet(null, State.STARTING)) {
String oldThreadName = Thread.currentThread().getName();
try {
+ executor.onStart(this);
+ runningState.set(State.RUNNING);
Thread.currentThread().setName(oldThreadName + "[" + task.toString() + "]");
task.run();
} finally {
Thread.currentThread().setName(oldThreadName);
+ runningState.set(State.STOPPING);
+ executor.onStop(this);
if (isPeriodic()) {
- running.set(false);
+ runningState.set(null);
} else {
+ runningState.set(State.DONE);
executor.remove(this);
}
}
diff --git a/java/com/google/gerrit/server/git/meta/TabFile.java b/java/com/google/gerrit/server/git/meta/TabFile.java
index 80570a5..5f76b39 100644
--- a/java/com/google/gerrit/server/git/meta/TabFile.java
+++ b/java/com/google/gerrit/server/git/meta/TabFile.java
@@ -17,6 +17,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.git.ValidationError;
import java.io.BufferedReader;
import java.io.IOException;
@@ -84,6 +85,7 @@
return map;
}
+ @Nullable
protected static String asText(String left, String right, Map<String, String> entries) {
if (entries.isEmpty()) {
return null;
@@ -96,6 +98,7 @@
return asText(left, right, rows);
}
+ @Nullable
protected static String asText(String left, String right, List<Row> rows) {
if (rows.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 26ae08a..093ca78 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -439,6 +439,7 @@
private MessageSender messageSender;
private ReceiveCommitsResult.Builder result;
private ImmutableMap<String, String> loggingTags;
+ private ImmutableList<String> transitionalPluginOptions;
/** This object is for single use only. */
private boolean used;
@@ -590,6 +591,8 @@
useRefCache
? ReceivePackRefCache.withAdvertisedRefs(() -> allRefsWatcher.getAllRefs())
: ReceivePackRefCache.noCache(receivePack.getRepository().getRefDatabase());
+ this.transitionalPluginOptions =
+ ImmutableList.copyOf(config.getStringList("plugins", null, "transitionalPushOptions"));
}
void init() {
@@ -2132,6 +2135,9 @@
}
private boolean isPluginPushOption(String pushOptionName) {
+ if (transitionalPluginOptions.contains(pushOptionName)) {
+ return true;
+ }
return StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
.anyMatch(e -> pushOptionName.equals(e.getPluginName() + "~" + e.get().getName()));
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index e545c70..c06622b 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -112,7 +112,7 @@
.setRequestedFields(
// Required for ChangeIsVisibleToPrdicate.
ChangeField.CHANGE,
- ChangeField.REVIEWER,
+ ChangeField.REVIEWER_SPEC,
// Required during advertiseOpenChanges.
ChangeField.PATCH_SET)
.enforceVisibility(true)
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 644f82e..6e5cfff 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -405,7 +405,7 @@
// Ignore failures for reasons like the reviewer being inactive or being unable to see the
// change. See discussion in ChangeInserter.
- input.otherFailureBehavior = ReviewerModifier.FailureBehavior.IGNORE;
+ input.otherFailureBehavior = ReviewerModifier.FailureBehavior.IGNORE_EXCEPT_NOT_FOUND;
return input;
}
@@ -441,6 +441,7 @@
update, message.toString(), ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
}
+ @Nullable
private String changeKindMessage(ChangeKind changeKind) {
switch (changeKind) {
case MERGE_FIRST_PARENT_UPDATE:
@@ -624,6 +625,7 @@
return cmd;
}
+ @Nullable
private static String findMergedInto(Context ctx, String first, RevCommit commit) {
try {
RevWalk rw = ctx.getRevWalk();
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index 546614c..001a153 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.group;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
@@ -84,6 +85,7 @@
* @param id ID of the group, can be a group UUID, a group name or a legacy group ID
* @return the group, null if no group is found for the given group ID
*/
+ @Nullable
public GroupDescription.Basic parseId(String id) {
logger.atFine().log("Parsing group %s", id);
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 5a9b9e5..0471acc 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -21,6 +21,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
@@ -140,6 +141,7 @@
return isSystemGroup(uuid);
}
+ @Nullable
@Override
public GroupDescription.Basic get(AccountGroup.UUID uuid) {
final GroupReference ref = uuids.get(uuid);
@@ -157,11 +159,13 @@
return ref.getUUID();
}
+ @Nullable
@Override
public String getUrl() {
return null;
}
+ @Nullable
@Override
public String getEmailAddress() {
return null;
diff --git a/java/com/google/gerrit/server/group/testing/BUILD b/java/com/google/gerrit/server/group/testing/BUILD
index 77bb777..bb2b20d 100644
--- a/java/com/google/gerrit/server/group/testing/BUILD
+++ b/java/com/google/gerrit/server/group/testing/BUILD
@@ -7,6 +7,7 @@
testonly = True,
srcs = glob(["*.java"]),
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/server",
"//lib:guava",
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
index 12d8c93..b0e270c 100644
--- a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -18,6 +18,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
@@ -32,7 +33,7 @@
/** Implementation of GroupBackend for tests. */
public class TestGroupBackend implements GroupBackend {
- private static final String PREFIX = "testbackend:";
+ public static final String PREFIX = "testbackend:";
private final Map<AccountGroup.UUID, GroupDescription.Basic> groups = new HashMap<>();
private final Map<Account.Id, GroupMembership> memberships = new HashMap<>();
@@ -111,6 +112,7 @@
return false;
}
+ @Nullable
@Override
public GroupDescription.Basic get(AccountGroup.UUID uuid) {
return uuid == null ? null : groups.get(uuid);
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 80cc463..7d40c00 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -16,7 +16,7 @@
import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
-import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -81,10 +81,10 @@
// A Change is always sufficient.
return fs;
}
- if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID_STR.getName())) {
+ if (fs.contains(PROJECT_SPEC.getName()) && fs.contains(LEGACY_ID_STR.getName())) {
return fs;
}
- return Sets.union(fs, ImmutableSet.of(LEGACY_ID_STR.getName(), PROJECT.getName()));
+ return Sets.union(fs, ImmutableSet.of(LEGACY_ID_STR.getName(), PROJECT_SPEC.getName()));
}
/**
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index c802205..ed58a0b 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -111,9 +111,7 @@
NAME_PART_NO_SECONDARY_EMAIL_SPEC = NAME_PART_NO_SECONDARY_EMAIL_FIELD.prefix("name2");
public static final IndexedField<AccountState, String> FULL_NAME_FIELD =
- IndexedField.<AccountState>stringBuilder("FullName")
- .required()
- .build(a -> a.account().fullName());
+ IndexedField.<AccountState>stringBuilder("FullName").build(a -> a.account().fullName());
public static final IndexedField<AccountState, String>.SearchSpec FULL_NAME_SPEC =
FULL_NAME_FIELD.exact("full_name");
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 6cdc9ae..03f4c50 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -26,6 +26,7 @@
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.index.SiteIndexer;
@@ -112,6 +113,10 @@
private static ProjectSlice create(Project.NameKey name, int slice, int slices, ScanResult sr) {
return new AutoValue_AllChangesIndexer_ProjectSlice(name, slice, slices, sr);
}
+
+ private static ProjectSlice oneSlice(Project.NameKey name, ScanResult sr) {
+ return new AutoValue_AllChangesIndexer_ProjectSlice(name, 0, 1, sr);
+ }
}
@Override
@@ -175,50 +180,39 @@
return Result.create(sw, ok.get(), nDone, nFailed);
}
+ @Nullable
public Callable<Void> reindexProject(
ChangeIndexer indexer, Project.NameKey project, Task done, Task failed) {
try (Repository repo = repoManager.openRepository(project)) {
- return reindexProject(
- indexer, project, 0, 1, ChangeNotes.Factory.scanChangeIds(repo), done, failed);
+ return reindexProjectSlice(
+ indexer,
+ ProjectSlice.oneSlice(project, ChangeNotes.Factory.scanChangeIds(repo)),
+ done,
+ failed);
} catch (IOException e) {
logger.atSevere().log("%s", e.getMessage());
return null;
}
}
- public Callable<Void> reindexProject(
- ChangeIndexer indexer,
- Project.NameKey project,
- int slice,
- int slices,
- ScanResult scanResult,
- Task done,
- Task failed) {
- return new ProjectIndexer(indexer, project, slice, slices, scanResult, done, failed);
+ public Callable<Void> reindexProjectSlice(
+ ChangeIndexer indexer, ProjectSlice projectSlice, Task done, Task failed) {
+ return new ProjectSliceIndexer(indexer, projectSlice, done, failed);
}
- private class ProjectIndexer implements Callable<Void> {
+ private class ProjectSliceIndexer implements Callable<Void> {
private final ChangeIndexer indexer;
- private final Project.NameKey project;
- private final int slice;
- private final int slices;
- private final ScanResult scanResult;
+ private final ProjectSlice projectSlice;
private final ProgressMonitor done;
private final ProgressMonitor failed;
- private ProjectIndexer(
+ private ProjectSliceIndexer(
ChangeIndexer indexer,
- Project.NameKey project,
- int slice,
- int slices,
- ScanResult scanResult,
+ ProjectSlice projectSlice,
ProgressMonitor done,
ProgressMonitor failed) {
this.indexer = indexer;
- this.project = project;
- this.slice = slice;
- this.slices = slices;
- this.scanResult = scanResult;
+ this.projectSlice = projectSlice;
this.done = done;
this.failed = failed;
}
@@ -232,7 +226,10 @@
// but the goal is to invalidate that cache as infrequently as we possibly can. And besides,
// we don't have concrete proof that improving packfile locality would help.
notesFactory
- .scan(scanResult, project, id -> (id.get() % slices) == slice)
+ .scan(
+ projectSlice.scanResult(),
+ projectSlice.name(),
+ id -> (id.get() % projectSlice.slices()) == projectSlice.slice())
.forEach(r -> index(r));
OnlineReindexMode.end();
return null;
@@ -271,7 +268,7 @@
@Override
public String toString() {
- return "Index all changes of project " + project.get();
+ return "Index project slice " + projectSlice;
}
}
@@ -351,12 +348,9 @@
ProjectSlice projectSlice = ProjectSlice.create(name, slice, slices, sr);
ListenableFuture<?> future =
executor.submit(
- reindexProject(
+ reindexProjectSlice(
indexerFactory.create(executor, index),
- name,
- slice,
- slices,
- projectSlice.scanResult(),
+ projectSlice,
doneTask,
failedTask));
String description = "project " + name + " (" + slice + "/" + slices + ")";
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index f1b0b96..e60a2fd 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -62,6 +62,7 @@
import com.google.gerrit.entities.converter.PatchSetProtoConverter;
import com.google.gerrit.entities.converter.ProtoConverter;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.RefState;
import com.google.gerrit.index.SchemaFieldDefs;
import com.google.gerrit.index.SchemaUtil;
@@ -128,46 +129,70 @@
// TODO: Rename LEGACY_ID to NUMERIC_ID
/** Legacy change ID. */
public static final FieldDef<ChangeData, String> LEGACY_ID_STR =
- exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getVirtualId().get()));
+ exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getId().get()));
/** Newer style Change-Id key. */
public static final FieldDef<ChangeData, String> ID =
prefix(ChangeQueryBuilder.FIELD_CHANGE_ID).build(changeGetter(c -> c.getKey().get()));
/** Change status string, in the same format as {@code status:}. */
- public static final FieldDef<ChangeData, String> STATUS =
- exact(ChangeQueryBuilder.FIELD_STATUS)
+ public static final IndexedField<ChangeData, String> STATUS_FIELD =
+ IndexedField.<ChangeData>stringBuilder("Status")
+ .required()
+ .size(20)
.build(changeGetter(c -> ChangeStatusPredicate.canonicalize(c.getStatus())));
+ public static final IndexedField<ChangeData, String>.SearchSpec STATUS_SPEC =
+ STATUS_FIELD.exact(ChangeQueryBuilder.FIELD_STATUS);
+
/** Project containing the change. */
- public static final FieldDef<ChangeData, String> PROJECT =
- exact(ChangeQueryBuilder.FIELD_PROJECT)
+ public static final IndexedField<ChangeData, String> PROJECT_FIELD =
+ IndexedField.<ChangeData>stringBuilder("Project")
+ .required()
.stored()
+ .size(200)
.build(changeGetter(c -> c.getProject().get()));
+ public static final IndexedField<ChangeData, String>.SearchSpec PROJECT_SPEC =
+ PROJECT_FIELD.exact(ChangeQueryBuilder.FIELD_PROJECT);
+
/** Project containing the change, as a prefix field. */
- public static final FieldDef<ChangeData, String> PROJECTS =
- prefix(ChangeQueryBuilder.FIELD_PROJECTS).build(changeGetter(c -> c.getProject().get()));
+ public static final IndexedField<ChangeData, String>.SearchSpec PROJECTS_SPEC =
+ PROJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PROJECTS);
/** Reference (aka branch) the change will submit onto. */
- public static final FieldDef<ChangeData, String> REF =
- exact(ChangeQueryBuilder.FIELD_REF).build(changeGetter(c -> c.getDest().branch()));
+ public static final IndexedField<ChangeData, String> REF_FIELD =
+ IndexedField.<ChangeData>stringBuilder("Ref")
+ .required()
+ .size(300)
+ .build(changeGetter(c -> c.getDest().branch()));
+
+ public static final IndexedField<ChangeData, String>.SearchSpec REF_SPEC =
+ REF_FIELD.exact(ChangeQueryBuilder.FIELD_REF);
/** Topic, a short annotation on the branch. */
- public static final FieldDef<ChangeData, String> EXACT_TOPIC =
- exact("topic4").build(ChangeField::getTopic);
+ public static final IndexedField<ChangeData, String> TOPIC_FIELD =
+ IndexedField.<ChangeData>stringBuilder("Topic").size(500).build(ChangeField::getTopic);
+
+ public static final IndexedField<ChangeData, String>.SearchSpec EXACT_TOPIC =
+ TOPIC_FIELD.exact("topic4");
/** Topic, a short annotation on the branch. */
- public static final FieldDef<ChangeData, String> FUZZY_TOPIC =
- fullText("topic5").build(ChangeField::getTopic);
+ public static final IndexedField<ChangeData, String>.SearchSpec FUZZY_TOPIC =
+ TOPIC_FIELD.fullText("topic5");
/** Topic, a short annotation on the branch. */
- public static final FieldDef<ChangeData, String> PREFIX_TOPIC =
- prefix("topic6").build(ChangeField::getTopic);
+ public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_TOPIC =
+ TOPIC_FIELD.prefix("topic6");
- /** Submission id assigned by MergeOp. */
- public static final FieldDef<ChangeData, String> SUBMISSIONID =
- exact(ChangeQueryBuilder.FIELD_SUBMISSIONID).build(changeGetter(Change::getSubmissionId));
+ /** {@link com.google.gerrit.entities.SubmissionId} assigned by MergeOp. */
+ public static final IndexedField<ChangeData, String> SUBMISSIONID_FIELD =
+ IndexedField.<ChangeData>stringBuilder("SubmissionId")
+ .size(500)
+ .build(changeGetter(Change::getSubmissionId));
+
+ public static final IndexedField<ChangeData, String>.SearchSpec SUBMISSIONID_SPEC =
+ SUBMISSIONID_FIELD.exact(ChangeQueryBuilder.FIELD_SUBMISSIONID);
/** Last update time since January 1, 1970. */
// TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
@@ -178,18 +203,26 @@
/** When this change was merged, time since January 1, 1970. */
// TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
- public static final FieldDef<ChangeData, Timestamp> MERGED_ON =
- timestamp(ChangeQueryBuilder.FIELD_MERGED_ON)
+ public static final IndexedField<ChangeData, Timestamp> MERGED_ON_FIELD =
+ IndexedField.<ChangeData>timestampBuilder("MergedOn")
.stored()
.build(
cd -> cd.getMergedOn().map(Timestamp::from).orElse(null),
(cd, field) -> cd.setMergedOn(field != null ? field.toInstant() : null));
+ public static final IndexedField<ChangeData, Timestamp>.SearchSpec MERGED_ON_SPEC =
+ MERGED_ON_FIELD.timestamp(ChangeQueryBuilder.FIELD_MERGED_ON);
+
/** List of full file paths modified in the current patch set. */
- public static final FieldDef<ChangeData, Iterable<String>> PATH =
+ public static final IndexedField<ChangeData, Iterable<String>> PATH_FIELD =
// Named for backwards compatibility.
- exact(ChangeQueryBuilder.FIELD_FILE)
- .buildRepeatable(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
+ IndexedField.<ChangeData>iterableStringBuilder("File")
+ .build(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PATH_SPEC =
+ PATH_FIELD
+ // Named for backwards compatibility.
+ .exact(ChangeQueryBuilder.FIELD_FILE);
public static Set<String> getFileParts(ChangeData cd) {
List<String> paths = cd.currentFilePaths();
@@ -205,24 +238,27 @@
}
/** Hashtags tied to a change */
- public static final FieldDef<ChangeData, Iterable<String>> HASHTAG =
- exact(ChangeQueryBuilder.FIELD_HASHTAG)
- .buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+ public static final IndexedField<ChangeData, Iterable<String>> HASHTAG_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("Hashtag")
+ .size(200)
+ .build(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec HASHTAG_SPEC =
+ HASHTAG_FIELD.exact(ChangeQueryBuilder.FIELD_HASHTAG);
/** Hashtags as fulltext field for in-string search. */
- public static final FieldDef<ChangeData, Iterable<String>> FUZZY_HASHTAG =
- fullText("hashtag2")
- .buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FUZZY_HASHTAG =
+ HASHTAG_FIELD.fullText("hashtag2");
/** Hashtags as prefix field for in-string search. */
- public static final FieldDef<ChangeData, Iterable<String>> PREFIX_HASHTAG =
- prefix("hashtag3")
- .buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PREFIX_HASHTAG =
+ HASHTAG_FIELD.prefix("hashtag3");
/** Hashtags with original case. */
- public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
- storedOnly("_hashtag")
- .buildRepeatable(
+ public static final IndexedField<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE_FIELD =
+ IndexedField.<ChangeData>iterableByteArrayBuilder("HashtagCaseAware")
+ .stored()
+ .build(
cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()),
(cd, field) ->
cd.setHashtags(
@@ -230,13 +266,24 @@
.map(f -> new String(f, UTF_8))
.collect(toImmutableSet())));
+ public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec
+ HASHTAG_CASE_AWARE_SPEC = HASHTAG_CASE_AWARE_FIELD.storedOnly("_hashtag");
+
/** Components of each file path modified in the current patch set. */
- public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
- exact(ChangeQueryBuilder.FIELD_FILEPART).buildRepeatable(ChangeField::getFileParts);
+ public static final IndexedField<ChangeData, Iterable<String>> FILE_PART_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("FilePart").build(ChangeField::getFileParts);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FILE_PART_SPEC =
+ FILE_PART_FIELD.exact(ChangeQueryBuilder.FIELD_FILEPART);
/** File extensions of each file modified in the current patch set. */
- public static final FieldDef<ChangeData, Iterable<String>> EXTENSION =
- exact(ChangeQueryBuilder.FIELD_EXTENSION).buildRepeatable(ChangeField::getExtensions);
+ public static final IndexedField<ChangeData, Iterable<String>> EXTENSION_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("Extension")
+ .size(100)
+ .build(ChangeField::getExtensions);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXTENSION_SPEC =
+ EXTENSION_FIELD.exact(ChangeQueryBuilder.FIELD_EXTENSION);
public static Set<String> getExtensions(ChangeData cd) {
return extensions(cd).collect(toSet());
@@ -246,8 +293,12 @@
* File extensions of each file modified in the current patch set as a sorted list. The purpose of
* this field is to allow matching changes that only touch files with certain file extensions.
*/
- public static final FieldDef<ChangeData, String> ONLY_EXTENSIONS =
- exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS).build(ChangeField::getAllExtensionsAsList);
+ public static final IndexedField<ChangeData, String> ONLY_EXTENSIONS_FIELD =
+ IndexedField.<ChangeData>stringBuilder("OnlyExtensions")
+ .build(ChangeField::getAllExtensionsAsList);
+
+ public static final IndexedField<ChangeData, String>.SearchSpec ONLY_EXTENSIONS_SPEC =
+ ONLY_EXTENSIONS_FIELD.exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS);
public static String getAllExtensionsAsList(ChangeData cd) {
return extensions(cd).distinct().sorted().collect(joining(","));
@@ -271,8 +322,11 @@
}
/** Footers from the commit message of the current patch set. */
- public static final FieldDef<ChangeData, Iterable<String>> FOOTER =
- exact(ChangeQueryBuilder.FIELD_FOOTER).buildRepeatable(ChangeField::getFooters);
+ public static final IndexedField<ChangeData, Iterable<String>> FOOTER_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("Footer").build(ChangeField::getFooters);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_SPEC =
+ FOOTER_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER);
public static Set<String> getFooters(ChangeData cd) {
return cd.commitFooters().stream()
@@ -281,16 +335,24 @@
}
/** Footers from the commit message of the current patch set. */
- public static final FieldDef<ChangeData, Iterable<String>> FOOTER_NAME =
- exact(ChangeQueryBuilder.FIELD_FOOTER_NAME).buildRepeatable(ChangeField::getFootersNames);
+ public static final IndexedField<ChangeData, Iterable<String>> FOOTER_NAME_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("FooterName")
+ .build(ChangeField::getFootersNames);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_NAME =
+ FOOTER_NAME_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER_NAME);
public static Set<String> getFootersNames(ChangeData cd) {
return cd.commitFooters().stream().map(f -> f.getKey()).collect(toSet());
}
/** Folders that are touched by the current patch set. */
- public static final FieldDef<ChangeData, Iterable<String>> DIRECTORY =
- exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories);
+ public static final IndexedField<ChangeData, Iterable<String>> DIRECTORY_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("Directory")
+ .build(ChangeField::getDirectories);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec DIRECTORY_SPEC =
+ DIRECTORY_FIELD.exact(ChangeQueryBuilder.FIELD_DIRECTORY);
public static Set<String> getDirectories(ChangeData cd) {
List<String> paths = cd.currentFilePaths();
@@ -325,31 +387,47 @@
}
/** Owner/creator of the change. */
- public static final FieldDef<ChangeData, Integer> OWNER =
- integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
+ public static final IndexedField<ChangeData, Integer> OWNER_FIELD =
+ IndexedField.<ChangeData>integerBuilder("Owner")
+ .required()
+ .build(changeGetter(c -> c.getOwner().get()));
+
+ public static final IndexedField<ChangeData, Integer>.SearchSpec OWNER_SPEC =
+ OWNER_FIELD.integer(ChangeQueryBuilder.FIELD_OWNER);
/** Uploader of the latest patch set. */
- public static final FieldDef<ChangeData, Integer> UPLOADER =
- integer(ChangeQueryBuilder.FIELD_UPLOADER).build(cd -> cd.currentPatchSet().uploader().get());
+ public static final IndexedField<ChangeData, Integer> UPLOADER_FIELD =
+ IndexedField.<ChangeData>integerBuilder("Uploader")
+ .required()
+ .build(cd -> cd.currentPatchSet().uploader().get());
+
+ public static final IndexedField<ChangeData, Integer>.SearchSpec UPLOADER_SPEC =
+ UPLOADER_FIELD.integer(ChangeQueryBuilder.FIELD_UPLOADER);
/** References the source change number that this change was cherry-picked from. */
- public static final FieldDef<ChangeData, Integer> CHERRY_PICK_OF_CHANGE =
- integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_CHANGE)
+ public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_CHANGE_FIELD =
+ IndexedField.<ChangeData>integerBuilder("CherryPickOfChange")
.build(
cd ->
cd.change().getCherryPickOf() != null
? cd.change().getCherryPickOf().changeId().get()
: null);
+ public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_CHANGE =
+ CHERRY_PICK_OF_CHANGE_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_CHANGE);
+
/** References the source change patch-set that this change was cherry-picked from. */
- public static final FieldDef<ChangeData, Integer> CHERRY_PICK_OF_PATCHSET =
- integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_PATCHSET)
+ public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_PATCHSET_FIELD =
+ IndexedField.<ChangeData>integerBuilder("CherryPickOfPatchset")
.build(
cd ->
cd.change().getCherryPickOf() != null
? cd.change().getCherryPickOf().get()
: null);
+ public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_PATCHSET =
+ CHERRY_PICK_OF_PATCHSET_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_PATCHSET);
+
/** This class decouples the internal and API types from storage. */
private static class StoredAttentionSetEntry {
final long timestampMillis;
@@ -376,15 +454,22 @@
*
* @see #ATTENTION_SET_FULL
*/
- public static final FieldDef<ChangeData, Iterable<Integer>> ATTENTION_SET_USERS =
- integer(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS)
- .buildRepeatable(ChangeField::getAttentionSetUserIds);
+ public static final IndexedField<ChangeData, Iterable<Integer>> ATTENTION_SET_USERS_FIELD =
+ IndexedField.<ChangeData>iterableIntegerBuilder("AttentionSetUsers")
+ .build(ChangeField::getAttentionSetUserIds);
+
+ public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec ATTENTION_SET_USERS =
+ ATTENTION_SET_USERS_FIELD.integer(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS);
/** Number of changes that contain attention set. */
- public static final FieldDef<ChangeData, Integer> ATTENTION_SET_USERS_COUNT =
- intRange(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS_COUNT)
+ public static final IndexedField<ChangeData, Integer> ATTENTION_SET_USERS_COUNT_FIELD =
+ IndexedField.<ChangeData>integerBuilder("AttentionSetUsersCount")
.build(cd -> additionsOnly(cd.attentionSet()).size());
+ public static final IndexedField<ChangeData, Integer>.SearchSpec ATTENTION_SET_USERS_COUNT =
+ ATTENTION_SET_USERS_COUNT_FIELD.integerRange(
+ ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS_COUNT);
+
/**
* The full attention set data including timestamp, reason and possible future fields.
*
@@ -402,60 +487,82 @@
cd));
/** The user assigned to the change. */
- public static final FieldDef<ChangeData, Integer> ASSIGNEE =
- integer(ChangeQueryBuilder.FIELD_ASSIGNEE)
+ public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD =
+ IndexedField.<ChangeData>integerBuilder("Assignee")
.build(changeGetter(c -> c.getAssignee() != null ? c.getAssignee().get() : NO_ASSIGNEE));
+ public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC =
+ ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE);
+
/** Reviewer(s) associated with the change. */
- public static final FieldDef<ChangeData, Iterable<String>> REVIEWER =
- exact("reviewer2")
+ public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("Reviewer")
.stored()
- .buildRepeatable(
+ .build(
cd -> getReviewerFieldValues(cd.reviewers()),
(cd, field) -> cd.setReviewers(parseReviewerFieldValues(cd.getId(), field)));
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_SPEC =
+ REVIEWER_FIELD.exact("reviewer2");
+
/** Reviewer(s) associated with the change that do not have a gerrit account. */
- public static final FieldDef<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL =
- exact("reviewer_by_email")
+ public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("ReviewerByEmail")
.stored()
- .buildRepeatable(
+ .build(
cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()),
(cd, field) ->
cd.setReviewersByEmail(parseReviewerByEmailFieldValues(cd.getId(), field)));
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_BY_EMAIL =
+ REVIEWER_BY_EMAIL_FIELD.exact("reviewer_by_email");
+
/** Reviewer(s) modified during change's current WIP phase. */
- public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER =
- exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER)
+ public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("PendingReviewer")
.stored()
- .buildRepeatable(
+ .build(
cd -> getReviewerFieldValues(cd.pendingReviewers()),
(cd, field) -> cd.setPendingReviewers(parseReviewerFieldValues(cd.getId(), field)));
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PENDING_REVIEWER_SPEC =
+ PENDING_REVIEWER_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER);
+
/** Reviewer(s) by email modified during change's current WIP phase. */
- public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL =
- exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL)
+ public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("PendingReviewerByEmail")
.stored()
- .buildRepeatable(
+ .build(
cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()),
(cd, field) ->
cd.setPendingReviewersByEmail(
parseReviewerByEmailFieldValues(cd.getId(), field)));
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec
+ PENDING_REVIEWER_BY_EMAIL =
+ PENDING_REVIEWER_BY_EMAIL_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL);
+
/** References a change that this change reverts. */
- public static final FieldDef<ChangeData, Integer> REVERT_OF =
- integer(ChangeQueryBuilder.FIELD_REVERTOF)
+ public static final IndexedField<ChangeData, Integer> REVERT_OF_FIELD =
+ IndexedField.<ChangeData>integerBuilder("RevertOf")
.build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
- public static final FieldDef<ChangeData, String> IS_PURE_REVERT =
- fullText(ChangeQueryBuilder.FIELD_PURE_REVERT)
+ public static final IndexedField<ChangeData, Integer>.SearchSpec REVERT_OF =
+ REVERT_OF_FIELD.integer(ChangeQueryBuilder.FIELD_REVERTOF);
+
+ public static final IndexedField<ChangeData, String> IS_PURE_REVERT_FIELD =
+ IndexedField.<ChangeData>stringBuilder("IsPureRevert")
.build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
+ public static final IndexedField<ChangeData, String>.SearchSpec IS_PURE_REVERT_SPEC =
+ IS_PURE_REVERT_FIELD.fullText(ChangeQueryBuilder.FIELD_PURE_REVERT);
+
/**
* Determines if a change is submittable based on {@link
* com.google.gerrit.entities.SubmitRequirement}s.
*/
- public static final FieldDef<ChangeData, String> IS_SUBMITTABLE =
- exact(ChangeQueryBuilder.FIELD_IS_SUBMITTABLE)
+ public static final IndexedField<ChangeData, String> IS_SUBMITTABLE_FIELD =
+ IndexedField.<ChangeData>stringBuilder("IsSubmittable")
.build(
cd ->
// All submit requirements should be fulfilled
@@ -464,6 +571,9 @@
? "1"
: "0");
+ public static final IndexedField<ChangeData, String>.SearchSpec IS_SUBMITTABLE_SPEC =
+ IS_SUBMITTABLE_FIELD.exact(ChangeQueryBuilder.FIELD_IS_SUBMITTABLE);
+
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
@@ -800,25 +910,49 @@
* The exact email address, or any part of the author name or email address, in the current patch
* set.
*/
- public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
- fullText(ChangeQueryBuilder.FIELD_AUTHOR).buildRepeatable(ChangeField::getAuthorParts);
+ public static final IndexedField<ChangeData, Iterable<String>> AUTHOR_PARTS_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("AuthorParts")
+ .required()
+ .description(
+ "The exact email address, or any part of the author name or email address, in the current patch set.")
+ .build(ChangeField::getAuthorParts);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec AUTHOR_PARTS_SPEC =
+ AUTHOR_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_AUTHOR);
/** The exact name, email address and NameEmail of the author. */
- public static final FieldDef<ChangeData, Iterable<String>> EXACT_AUTHOR =
- exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR)
- .buildRepeatable(ChangeField::getAuthorNameAndEmail);
+ public static final IndexedField<ChangeData, Iterable<String>> EXACT_AUTHOR_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("ExactAuthor")
+ .required()
+ .description("The exact name, email address and NameEmail of the author.")
+ .build(ChangeField::getAuthorNameAndEmail);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_AUTHOR_SPEC =
+ EXACT_AUTHOR_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR);
/**
* The exact email address, or any part of the committer name or email address, in the current
* patch set.
*/
- public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
- fullText(ChangeQueryBuilder.FIELD_COMMITTER).buildRepeatable(ChangeField::getCommitterParts);
+ public static final IndexedField<ChangeData, Iterable<String>> COMMITTER_PARTS_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("CommitterParts")
+ .description(
+ "The exact email address, or any part of the committer name or email address, in the current patch set.")
+ .required()
+ .build(ChangeField::getCommitterParts);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMITTER_PARTS_SPEC =
+ COMMITTER_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMITTER);
/** The exact name, email address, and NameEmail of the committer. */
- public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMITTER =
- exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER)
- .buildRepeatable(ChangeField::getCommitterNameAndEmail);
+ public static final IndexedField<ChangeData, Iterable<String>> EXACT_COMMITTER_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("ExactCommiter")
+ .required()
+ .description("The exact name, email address, and NameEmail of the committer.")
+ .build(ChangeField::getCommitterNameAndEmail);
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_COMMITTER_SPEC =
+ EXACT_COMMITTER_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER);
/** Serialized change object, used for pre-populating results. */
public static final FieldDef<ChangeData, byte[]> CHANGE =
@@ -1368,6 +1502,7 @@
},
(cd, field) -> cd.setRefStatePatterns(field));
+ @Nullable
private static String getTopic(ChangeData cd) {
Change c = cd.change();
if (c == null) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 05fb780..bb4b24c 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -188,6 +188,7 @@
* @throws QueryParseException if the underlying index implementation does not support this
* predicate.
*/
+ @Nullable
private Predicate<ChangeData> rewriteImpl(
Predicate<ChangeData> in, ChangeIndex index, QueryOptions opts, MutableInteger leafTerms)
throws QueryParseException {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 6849831..4b88919 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -407,6 +407,7 @@
return future;
}
+ @Nullable
@Override
public ChangeData callImpl() throws Exception {
// Remove this task from queuedIndexTasks. This is done right at the beginning of this task so
@@ -460,6 +461,7 @@
this.id = id;
}
+ @Nullable
@Override
public ChangeData call() {
logger.atFine().log("Delete change %d from index.", id.get());
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 6116f5a..3e43505 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -16,6 +16,8 @@
import static com.google.gerrit.index.SchemaUtil.schema;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaDefinitions;
import com.google.gerrit.server.query.change.ChangeData;
@@ -27,84 +29,119 @@
* com.google.gerrit.index.IndexUpgradeValidator}.
*/
public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
- /** Added new field {@link ChangeField#IS_SUBMITTABLE} based on submit requirements. */
+ /** Added new field {@link ChangeField#IS_SUBMITTABLE_SPEC} based on submit requirements. */
@Deprecated
static final Schema<ChangeData> V74 =
schema(
/* version= */ 74,
- ChangeField.ADDED,
- ChangeField.APPROVAL,
- ChangeField.ASSIGNEE,
- ChangeField.ATTENTION_SET_FULL,
- ChangeField.ATTENTION_SET_USERS,
- ChangeField.ATTENTION_SET_USERS_COUNT,
- ChangeField.AUTHOR,
- ChangeField.CHANGE,
- ChangeField.CHERRY_PICK,
- ChangeField.CHERRY_PICK_OF_CHANGE,
- ChangeField.CHERRY_PICK_OF_PATCHSET,
- ChangeField.COMMENT,
- ChangeField.COMMENTBY,
- ChangeField.COMMIT,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMITTER,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.DIRECTORY,
- ChangeField.DRAFTBY,
- ChangeField.EDITBY,
- ChangeField.EXACT_AUTHOR,
- ChangeField.EXACT_COMMIT,
- ChangeField.EXACT_COMMITTER,
- ChangeField.EXACT_TOPIC,
- ChangeField.EXTENSION,
- ChangeField.FILE_PART,
- ChangeField.FOOTER,
- ChangeField.FUZZY_HASHTAG,
- ChangeField.FUZZY_TOPIC,
- ChangeField.GROUP,
- ChangeField.HASHTAG,
- ChangeField.HASHTAG_CASE_AWARE,
- ChangeField.ID,
- ChangeField.IS_PURE_REVERT,
- ChangeField.IS_SUBMITTABLE,
- ChangeField.LABEL,
- ChangeField.LEGACY_ID_STR,
- ChangeField.MERGE,
- ChangeField.MERGEABLE,
- ChangeField.MERGED_ON,
- ChangeField.ONLY_EXTENSIONS,
- ChangeField.OWNER,
- ChangeField.PATCH_SET,
- ChangeField.PATH,
- ChangeField.PENDING_REVIEWER,
- ChangeField.PENDING_REVIEWER_BY_EMAIL,
- ChangeField.PRIVATE,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.REF_STATE,
- ChangeField.REF_STATE_PATTERN,
- ChangeField.REVERT_OF,
- ChangeField.REVIEWEDBY,
- ChangeField.REVIEWER,
- ChangeField.REVIEWER_BY_EMAIL,
- ChangeField.STAR,
- ChangeField.STARBY,
- ChangeField.STARTED,
- ChangeField.STATUS,
- ChangeField.STORED_SUBMIT_RECORD_LENIENT,
- ChangeField.STORED_SUBMIT_RECORD_STRICT,
- ChangeField.STORED_SUBMIT_REQUIREMENTS,
- ChangeField.SUBMISSIONID,
- ChangeField.SUBMIT_RECORD,
- ChangeField.SUBMIT_RULE_RESULT,
- ChangeField.TOTAL_COMMENT_COUNT,
- ChangeField.TR,
- ChangeField.UNRESOLVED_COMMENT_COUNT,
- ChangeField.UPDATED,
- ChangeField.UPLOADER,
- ChangeField.WIP);
+ ImmutableList.of(
+ ChangeField.ADDED,
+ ChangeField.APPROVAL,
+ ChangeField.ATTENTION_SET_FULL,
+ ChangeField.CHANGE,
+ ChangeField.CHERRY_PICK,
+ ChangeField.COMMENT,
+ ChangeField.COMMENTBY,
+ ChangeField.COMMIT,
+ ChangeField.COMMIT_MESSAGE,
+ ChangeField.DELETED,
+ ChangeField.DELTA,
+ ChangeField.DRAFTBY,
+ ChangeField.EDITBY,
+ ChangeField.EXACT_COMMIT,
+ ChangeField.GROUP,
+ ChangeField.ID,
+ ChangeField.LABEL,
+ ChangeField.LEGACY_ID_STR,
+ ChangeField.MERGE,
+ ChangeField.MERGEABLE,
+ ChangeField.PATCH_SET,
+ ChangeField.PRIVATE,
+ ChangeField.REF_STATE,
+ ChangeField.REF_STATE_PATTERN,
+ ChangeField.REVIEWEDBY,
+ ChangeField.STAR,
+ ChangeField.STARBY,
+ ChangeField.STARTED,
+ ChangeField.STORED_SUBMIT_RECORD_LENIENT,
+ ChangeField.STORED_SUBMIT_RECORD_STRICT,
+ ChangeField.STORED_SUBMIT_REQUIREMENTS,
+ ChangeField.SUBMIT_RECORD,
+ ChangeField.SUBMIT_RULE_RESULT,
+ ChangeField.TOTAL_COMMENT_COUNT,
+ ChangeField.TR,
+ ChangeField.UNRESOLVED_COMMENT_COUNT,
+ ChangeField.UPDATED,
+ ChangeField.WIP),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(
+ ChangeField.ASSIGNEE_FIELD,
+ ChangeField.ATTENTION_SET_USERS_COUNT_FIELD,
+ ChangeField.ATTENTION_SET_USERS_FIELD,
+ ChangeField.AUTHOR_PARTS_FIELD,
+ ChangeField.CHERRY_PICK_OF_CHANGE_FIELD,
+ ChangeField.CHERRY_PICK_OF_PATCHSET_FIELD,
+ ChangeField.COMMITTER_PARTS_FIELD,
+ ChangeField.DIRECTORY_FIELD,
+ ChangeField.EXACT_AUTHOR_FIELD,
+ ChangeField.EXACT_COMMITTER_FIELD,
+ ChangeField.EXTENSION_FIELD,
+ ChangeField.FILE_PART_FIELD,
+ ChangeField.FOOTER_FIELD,
+ ChangeField.HASHTAG_CASE_AWARE_FIELD,
+ ChangeField.HASHTAG_FIELD,
+ ChangeField.IS_PURE_REVERT_FIELD,
+ ChangeField.IS_SUBMITTABLE_FIELD,
+ ChangeField.MERGED_ON_FIELD,
+ ChangeField.ONLY_EXTENSIONS_FIELD,
+ ChangeField.OWNER_FIELD,
+ ChangeField.PATH_FIELD,
+ ChangeField.PENDING_REVIEWER_BY_EMAIL_FIELD,
+ ChangeField.PENDING_REVIEWER_FIELD,
+ ChangeField.PROJECT_FIELD,
+ ChangeField.REF_FIELD,
+ ChangeField.REVERT_OF_FIELD,
+ ChangeField.REVIEWER_BY_EMAIL_FIELD,
+ ChangeField.REVIEWER_FIELD,
+ ChangeField.STATUS_FIELD,
+ ChangeField.SUBMISSIONID_FIELD,
+ ChangeField.TOPIC_FIELD,
+ ChangeField.UPLOADER_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.ASSIGNEE_SPEC,
+ ChangeField.ATTENTION_SET_USERS,
+ ChangeField.ATTENTION_SET_USERS_COUNT,
+ ChangeField.AUTHOR_PARTS_SPEC,
+ ChangeField.CHERRY_PICK_OF_CHANGE,
+ ChangeField.CHERRY_PICK_OF_PATCHSET,
+ ChangeField.COMMITTER_PARTS_SPEC,
+ ChangeField.DIRECTORY_SPEC,
+ ChangeField.EXACT_AUTHOR_SPEC,
+ ChangeField.EXACT_COMMITTER_SPEC,
+ ChangeField.EXACT_TOPIC,
+ ChangeField.EXTENSION_SPEC,
+ ChangeField.FILE_PART_SPEC,
+ ChangeField.FOOTER_SPEC,
+ ChangeField.FUZZY_HASHTAG,
+ ChangeField.FUZZY_TOPIC,
+ ChangeField.HASHTAG_CASE_AWARE_SPEC,
+ ChangeField.HASHTAG_SPEC,
+ ChangeField.IS_PURE_REVERT_SPEC,
+ ChangeField.IS_SUBMITTABLE_SPEC,
+ ChangeField.MERGED_ON_SPEC,
+ ChangeField.ONLY_EXTENSIONS_SPEC,
+ ChangeField.OWNER_SPEC,
+ ChangeField.PATH_SPEC,
+ ChangeField.PENDING_REVIEWER_BY_EMAIL,
+ ChangeField.PENDING_REVIEWER_SPEC,
+ ChangeField.PROJECTS_SPEC,
+ ChangeField.PROJECT_SPEC,
+ ChangeField.REF_SPEC,
+ ChangeField.REVERT_OF,
+ ChangeField.REVIEWER_BY_EMAIL,
+ ChangeField.REVIEWER_SPEC,
+ ChangeField.STATUS_SPEC,
+ ChangeField.SUBMISSIONID_SPEC,
+ ChangeField.UPLOADER_SPEC));
/**
* Added new field {@link ChangeField#PREFIX_HASHTAG} and {@link ChangeField#PREFIX_TOPIC} to
@@ -114,14 +151,18 @@
static final Schema<ChangeData> V75 =
new Schema.Builder<ChangeData>()
.add(V74)
- .add(ChangeField.PREFIX_HASHTAG)
- .add(ChangeField.PREFIX_TOPIC)
+ .addSearchSpecs(ChangeField.PREFIX_HASHTAG)
+ .addSearchSpecs(ChangeField.PREFIX_TOPIC)
.build();
/** Added new field {@link ChangeField#FOOTER_NAME}. */
@Deprecated
static final Schema<ChangeData> V76 =
- new Schema.Builder<ChangeData>().add(V75).add(ChangeField.FOOTER_NAME).build();
+ new Schema.Builder<ChangeData>()
+ .add(V75)
+ .addIndexedFields(ChangeField.FOOTER_NAME_FIELD)
+ .addSearchSpecs(ChangeField.FOOTER_NAME)
+ .build();
/** Added new field {@link ChangeField#COMMIT_MESSAGE_EXACT}. */
@Deprecated
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 339d7bb..76610f3 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -16,7 +16,7 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
-import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -69,9 +69,9 @@
int limit,
Set<String> fields) {
// Always include project since it is needed to load the change from NoteDb.
- if (!fields.contains(CHANGE.getName()) && !fields.contains(PROJECT.getName())) {
+ if (!fields.contains(CHANGE.getName()) && !fields.contains(PROJECT_SPEC.getName())) {
fields = new HashSet<>(fields);
- fields.add(PROJECT.getName());
+ fields.add(PROJECT_SPEC.getName());
}
return QueryOptions.create(config, start, pageSize, pageSizeMultiplier, limit, fields);
}
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index fd0c4f1..6b9ecdf 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//lib:automaton",
"//lib:guava",
diff --git a/java/com/google/gerrit/server/ioutil/BasicSerialization.java b/java/com/google/gerrit/server/ioutil/BasicSerialization.java
index 296cf22..b43655a 100644
--- a/java/com/google/gerrit/server/ioutil/BasicSerialization.java
+++ b/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -31,6 +31,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.CodedEnum;
import java.io.EOFException;
import java.io.IOException;
@@ -129,6 +130,7 @@
}
/** Read a UTF-8 string, prefixed by its byte length in a varint. */
+ @Nullable
public static String readString(InputStream input) throws IOException {
final byte[] bin = readBytes(input);
if (bin.length == 0) {
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 652766a..6fe3cbe 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail.send;
import com.google.common.base.Joiner;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.EmailException;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.server.IdentifiedUser;
@@ -111,10 +112,12 @@
return "Unknown";
}
+ @Nullable
private String getSshKey() {
return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
}
+ @Nullable
private String getGpgKeys() {
if (gpgKeys != null) {
return Joiner.on("\n").join(gpgKeys);
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 3c821cc..79696fe 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -87,16 +87,19 @@
public List<Comment> comments = new ArrayList<>();
/** Returns a web link to a comment for a change. */
+ @Nullable
public String getCommentLink(String uuid) {
return args.urlFormatter.get().getInlineCommentView(change, uuid).orElse(null);
}
/** Returns a web link to the comment tab view of a change. */
+ @Nullable
public String getCommentsTabLink() {
return args.urlFormatter.get().getCommentsTabView(change).orElse(null);
}
/** Returns a web link to the findings tab view of a change. */
+ @Nullable
public String getFindingsTabLink() {
return args.urlFormatter.get().getFindingsTabView(change).orElse(null);
}
@@ -505,6 +508,7 @@
return false;
}
+ @Nullable
private Repository getRepository() {
try {
return args.server.openRepository(projectState.getNameKey());
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index d6d306c..64a01ff 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail.send;
import com.google.common.base.Joiner;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.EmailException;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.server.IdentifiedUser;
@@ -109,10 +110,12 @@
throw new IllegalStateException("key type is not SSH or GPG");
}
+ @Nullable
private String getSshKey() {
return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
}
+ @Nullable
private String getGpgKeyFingerprints() {
if (!gpgKeyFingerprints.isEmpty()) {
return Joiner.on("\n").join(gpgKeyFingerprints);
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index 70676e3..bd79d3a 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail.send;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.Change;
@@ -73,6 +74,7 @@
}
}
+ @Nullable
public List<String> getReviewerNames() {
if (reviewers.isEmpty() && reviewersByEmail.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index e899fc5..800066e 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail.send;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.exceptions.EmailException;
@@ -96,6 +97,7 @@
}
}
+ @Nullable
public List<String> getReviewerNames() {
if (reviewers.isEmpty()) {
return null;
@@ -107,6 +109,7 @@
return names;
}
+ @Nullable
public List<String> getRemovedReviewerNames() {
if (removedReviewers.isEmpty() && removedByEmailReviewers.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 5b209ce..f023075 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -17,6 +17,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.BranchNameKey;
@@ -92,6 +93,7 @@
protected abstract void addWatcher(RecipientType type, Account.Id to);
+ @Nullable
public String getSshHost() {
String host = Iterables.getFirst(args.sshAddresses, null);
if (host == null) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index bfc1f5b..55f82d4 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -380,10 +380,12 @@
return SystemReader.getInstance().getHostname();
}
+ @Nullable
public String getSettingsUrl() {
return args.urlFormatter.get().getSettingsUrl().orElse(null);
}
+ @Nullable
private String getGerritUrl() {
return args.urlFormatter.get().getWebUrl().orElse(null);
}
@@ -471,6 +473,7 @@
* @param accountId user to fetch.
* @return name/email of account, username, or null if unset or the accountId is null.
*/
+ @Nullable
protected String getUserNameEmailFor(@Nullable Account.Id accountId) {
if (accountId == null) {
return null;
@@ -594,6 +597,7 @@
}
}
+ @Nullable
private Address toAddress(Account.Id id) {
Optional<Account> accountState = args.accountCache.get(id).map(AccountState::account);
if (!accountState.isPresent()) {
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 0d32dd5..5f31c68 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -142,6 +142,7 @@
}
}
+ @Nullable
public ImmutableList<String> getReviewerNames() {
List<String> names = new ArrayList<>();
for (Account.Id id : reviewers) {
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 93f29f6..2edea26 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -17,7 +17,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Change;
@@ -53,7 +53,7 @@
public final AllUsersName allUsers;
public final NoteDbMetrics metrics;
public final String serverId;
- public final ImmutableList<String> importedServerIds;
+ public final ImmutableSet<String> importedServerIds;
// Providers required to avoid dependency cycles.
@@ -68,7 +68,7 @@
NoteDbMetrics metrics,
Provider<ChangeNotesCache> cache,
@GerritServerId String serverId,
- @GerritImportedServerIds ImmutableList<String> importedServerIds) {
+ @GerritImportedServerIds ImmutableSet<String> importedServerIds) {
this.failOnLoadForTest = new AtomicBoolean();
this.repoManager = repoManager;
this.allUsers = allUsers;
@@ -204,6 +204,7 @@
return load();
}
+ @Nullable
public ObjectId loadRevision() {
if (loaded) {
return getRevision();
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index e6f1622..ba91c68 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -101,6 +101,7 @@
user);
}
+ @Nullable
private static Account.Id accountId(CurrentUser u) {
checkUserType(u);
return (u instanceof IdentifiedUser) ? u.getAccountId() : null;
@@ -206,6 +207,7 @@
* deleted.
* @throws IOException if a lower-level error occurred.
*/
+ @Nullable
final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException {
if (isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 73161d7..0dcf786 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -19,6 +19,7 @@
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
@@ -187,6 +188,7 @@
return clonedUpdate;
}
+ @Nullable
private CommitBuilder storeCommentsInNotes(
RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
throws ConfigInvalidException, IOException {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
new file mode 100644
index 0000000..771d72b
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2022 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.notedb;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+
+/** Footers, that can be set in NoteDb commits. */
+public class ChangeNoteFooters {
+ public static final FooterKey FOOTER_ATTENTION = new FooterKey("Attention");
+ public static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
+ public static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
+ public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
+ public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
+ public static final FooterKey FOOTER_CURRENT = new FooterKey("Current");
+ public static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
+ public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
+ public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
+ public static final FooterKey FOOTER_COPIED_LABEL = new FooterKey("Copied-Label");
+ public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
+ public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
+ new FooterKey("Patch-set-description");
+ public static final FooterKey FOOTER_PRIVATE = new FooterKey("Private");
+ public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
+ public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
+ public static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
+ public static final FooterKey FOOTER_SUBMISSION_ID = new FooterKey("Submission-id");
+ public static final FooterKey FOOTER_SUBMITTED_WITH = new FooterKey("Submitted-with");
+ public static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
+ public static final FooterKey FOOTER_TAG = new FooterKey("Tag");
+ public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
+ public static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
+ public static final FooterKey FOOTER_CHERRY_PICK_OF = new FooterKey("Cherry-pick-of");
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index a30cfe0..8f6ad67 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -15,8 +15,6 @@
package com.google.gerrit.server.notedb;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.json.OutputFormat;
@@ -24,41 +22,13 @@
import com.google.gson.Gson;
import com.google.inject.Inject;
import java.time.Instant;
-import java.util.List;
import java.util.Optional;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.RawParseUtils;
public class ChangeNoteUtil {
- public static final FooterKey FOOTER_ATTENTION = new FooterKey("Attention");
- public static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
- public static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
- public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
- public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
- public static final FooterKey FOOTER_CURRENT = new FooterKey("Current");
- public static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
- public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
- public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
- public static final FooterKey FOOTER_COPIED_LABEL = new FooterKey("Copied-Label");
- public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
- public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
- new FooterKey("Patch-set-description");
- public static final FooterKey FOOTER_PRIVATE = new FooterKey("Private");
- public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
- public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
- public static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
- public static final FooterKey FOOTER_SUBMISSION_ID = new FooterKey("Submission-id");
- public static final FooterKey FOOTER_SUBMITTED_WITH = new FooterKey("Submitted-with");
- public static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
- public static final FooterKey FOOTER_TAG = new FooterKey("Tag");
- public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
- public static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
- public static final FooterKey FOOTER_CHERRY_PICK_OF = new FooterKey("Cherry-pick-of");
-
static final String GERRIT_USER_TEMPLATE = "Gerrit User %d";
private static final Gson gson = OutputFormat.JSON_COMPACT.newGson();
@@ -250,212 +220,4 @@
new AttentionStatusInNoteDb(
stringBuilder.toString(), attentionSetUpdate.operation(), attentionSetUpdate.reason()));
}
-
- /**
- * {@link com.google.gerrit.entities.PatchSetApproval}, parsed from {@link #FOOTER_LABEL} or
- * {@link #FOOTER_COPIED_LABEL}.
- *
- * <p>In comparison to {@link com.google.gerrit.entities.PatchSetApproval}, this entity represent
- * the raw fields, parsed from the NoteDB footer line, without any interpretation of the parsed
- * values. See {@link #parseApproval} and {@link #parseCopiedApproval} for the valid {@link
- * #footerLine} values.
- */
- @AutoValue
- public abstract static class ParsedPatchSetApproval {
-
- /** The original footer value, that this entity was parsed from. */
- public abstract String footerLine();
-
- public abstract boolean isRemoval();
-
- /** Either <LABEL>=VOTE or <LABEL> for {@link #isRemoval}. */
- public abstract String labelVote();
-
- public abstract Optional<String> uuid();
-
- public abstract Optional<String> accountIdent();
-
- public abstract Optional<String> realAccountIdent();
-
- public abstract Optional<String> tag();
-
- public static Builder builder() {
- return new AutoValue_ChangeNoteUtil_ParsedPatchSetApproval.Builder();
- }
-
- @AutoValue.Builder
- public abstract static class Builder {
-
- abstract Builder footerLine(String labelLine);
-
- abstract Builder isRemoval(boolean isRemoval);
-
- abstract Builder labelVote(String labelVote);
-
- abstract Builder uuid(Optional<String> uuid);
-
- abstract Builder accountIdent(Optional<String> accountIdent);
-
- abstract Builder realAccountIdent(Optional<String> realAccountIdent);
-
- abstract Builder tag(Optional<String> tag);
-
- abstract ParsedPatchSetApproval build();
- }
- }
-
- /**
- * Parses {@link ParsedPatchSetApproval} from {@link #FOOTER_LABEL} line.
- *
- * <p>Valid added approval footer examples:
- *
- * <ul>
- * <li>Label: <LABEL>=VOTE
- * <li>Label: <LABEL>=VOTE <Gerrit Account>
- * <li>Label: <LABEL>=VOTE, <UUID>
- * <li>Label: <LABEL>=VOTE, <UUID> <Gerrit Account>
- * </ul>
- *
- * <p>Valid removed approval footer examples:
- *
- * <ul>
- * <li>-<LABEL>
- * <li>-<LABEL> <Gerrit Account>
- * </ul>
- *
- * <p><UUID> is optional, since the approval might have been granted before {@link
- * com.google.gerrit.entities.PatchSetApproval.UUID} was introduced.
- *
- * <p><Gerrit Account> is only persisted in cases, when the account, that granted the vote does
- * not match the account, that issued {@link ChangeUpdate} (created this NoteDB commit).
- */
- public static ParsedPatchSetApproval parseApproval(String footerLine)
- throws ConfigInvalidException {
- try {
- ParsedPatchSetApproval.Builder rawPatchSetApproval =
- ParsedPatchSetApproval.builder().footerLine(footerLine);
- String labelVoteStr;
- boolean isRemoval = footerLine.startsWith("-");
- rawPatchSetApproval.isRemoval(isRemoval);
- int uuidStart = isRemoval ? -1 : footerLine.indexOf(", ");
- int reviewerStart = footerLine.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
- int labelStart = isRemoval ? 1 : 0;
- checkFooter(!isRemoval || uuidStart == -1, FOOTER_LABEL, footerLine);
-
- if (uuidStart != -1) {
- String uuid =
- footerLine.substring(
- uuidStart + 2, reviewerStart > 0 ? reviewerStart : footerLine.length());
- checkFooter(!Strings.isNullOrEmpty(uuid), FOOTER_LABEL, footerLine);
- labelVoteStr = footerLine.substring(labelStart, uuidStart);
- rawPatchSetApproval.uuid(Optional.of(uuid));
- } else if (reviewerStart != -1) {
- labelVoteStr = footerLine.substring(labelStart, reviewerStart);
- } else {
- labelVoteStr = footerLine.substring(labelStart);
- }
- rawPatchSetApproval.labelVote(labelVoteStr);
-
- if (reviewerStart > 0) {
- String ident = footerLine.substring(reviewerStart + 1);
- rawPatchSetApproval.accountIdent(Optional.of(ident));
- }
- return rawPatchSetApproval.build();
- } catch (StringIndexOutOfBoundsException ex) {
- throw parseException(FOOTER_LABEL, footerLine, ex);
- }
- }
-
- /**
- * Parses copied {@link ParsedPatchSetApproval} from {@link #FOOTER_COPIED_LABEL} line.
- *
- * <p>Footer example: Copied-Label: <LABEL>=VOTE, <UUID> <Gerrit Account>,<Gerrit Real Account>
- * :"<TAG>"
- *
- * <ul>
- * <li>":<"TAG>"" is optional.
- * <li><Gerrit Real Account> is also optional, if it was not set.
- * <li><UUID> is optional, since the approval might have been granted before {@link
- * com.google.gerrit.entities.PatchSetApproval.UUID} was introduced.
- * <li>The label, vote, and the Gerrit account are mandatory (unlike FOOTER_LABEL where Gerrit
- * Account is also optional since by default it's the committer).
- * </ul>
- *
- * <p>Footer example for removal: Copied-Label: -<LABEL> <Gerrit Account>,<Gerrit Real Account>
- *
- * <ul>
- * <li><Gerrit Real Account> is also optional, if it was not set.
- * </ul>
- */
- public static ParsedPatchSetApproval parseCopiedApproval(String labelLine)
- throws ConfigInvalidException {
- try {
- ParsedPatchSetApproval.Builder rawPatchSetApproval =
- ParsedPatchSetApproval.builder().footerLine(labelLine);
-
- boolean isRemoval = labelLine.startsWith("-");
- rawPatchSetApproval.isRemoval(isRemoval);
- int labelStart = isRemoval ? 1 : 0;
- int uuidStart = isRemoval ? -1 : labelLine.indexOf(", ");
- int tagStart = isRemoval ? -1 : labelLine.indexOf(":\"");
-
- checkFooter(!isRemoval || uuidStart == -1, FOOTER_LABEL, labelLine);
-
- // Weird tag that contains uuid delimiter. The uuid is actually not present.
- if (tagStart != -1 && uuidStart > tagStart) {
- uuidStart = -1;
- }
-
- int identitiesStart = labelLine.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
- checkFooter(
- identitiesStart != -1 && identitiesStart < labelLine.length(),
- FOOTER_COPIED_LABEL,
- labelLine);
-
- String labelVoteStr =
- labelLine.substring(labelStart, uuidStart != -1 ? uuidStart : identitiesStart);
- rawPatchSetApproval.labelVote(labelVoteStr);
- if (uuidStart != -1) {
- String uuid = labelLine.substring(uuidStart + 2, identitiesStart);
- checkFooter(!Strings.isNullOrEmpty(uuid), FOOTER_COPIED_LABEL, labelLine);
- rawPatchSetApproval.uuid(Optional.of(uuid));
- }
- // The first account is the accountId, and second (if applicable) is the realAccountId.
- List<String> identities =
- Splitter.on(',')
- .splitToList(
- labelLine.substring(
- identitiesStart + 1, tagStart == -1 ? labelLine.length() : tagStart));
- checkFooter(identities.size() >= 1, FOOTER_COPIED_LABEL, labelLine);
-
- rawPatchSetApproval.accountIdent(Optional.of(identities.get(0)));
-
- if (identities.size() > 1) {
- rawPatchSetApproval.realAccountIdent(Optional.of(identities.get(1)));
- }
-
- if (tagStart != -1) {
- // tagStart+2 skips ":\"" to parse the actual tag. Tags are in brackets.
- // line.length()-1 skips the last ".
- String tag = labelLine.substring(tagStart + 2, labelLine.length() - 1);
- rawPatchSetApproval.tag(Optional.of(tag));
- }
- return rawPatchSetApproval.build();
- } catch (StringIndexOutOfBoundsException ex) {
- throw parseException(FOOTER_COPIED_LABEL, labelLine, ex);
- }
- }
-
- private static void checkFooter(boolean expr, FooterKey footer, String actual)
- throws ConfigInvalidException {
- if (!expr) {
- throw parseException(footer, actual, /*cause=*/ null);
- }
- }
-
- private static ConfigInvalidException parseException(
- FooterKey footer, String actual, Throwable cause) {
- return new ConfigInvalidException(
- String.format("invalid %s: %s", footer.getName(), actual), cause);
- }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index a342686..ecdda71 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -412,8 +412,7 @@
public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
if (patchSets == null) {
- ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
- ImmutableSortedMap.orderedBy(comparing(PatchSet.Id::get));
+ ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b = ImmutableSortedMap.naturalOrder();
b.putAll(state.patchSets());
patchSets = b.build();
}
@@ -679,6 +678,7 @@
return change.getProject();
}
+ @Nullable
@Override
protected ObjectId readRef(Repository repo) throws IOException {
return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo);
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
index 76573f6..38ab8e9 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -15,8 +15,8 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParseApprovalUtil.java b/java/com/google/gerrit/server/notedb/ChangeNotesParseApprovalUtil.java
new file mode 100644
index 0000000..83ee6ec
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParseApprovalUtil.java
@@ -0,0 +1,242 @@
+// Copyright (C) 2022 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.notedb;
+
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.revwalk.FooterKey;
+
+/**
+ * Util to extract {@link com.google.gerrit.entities.PatchSetApproval} from {@link
+ * ChangeNoteFooters#FOOTER_LABEL} or {@link ChangeNoteFooters#FOOTER_COPIED_LABEL}.
+ */
+public class ChangeNotesParseApprovalUtil {
+
+ /**
+ * {@link com.google.gerrit.entities.PatchSetApproval}, parsed from {@link
+ * ChangeNoteFooters#FOOTER_LABEL} or {@link ChangeNoteFooters#FOOTER_COPIED_LABEL}.
+ *
+ * <p>In comparison to {@link com.google.gerrit.entities.PatchSetApproval}, this entity represent
+ * the raw fields, parsed from the NoteDB footer line, without any interpretation of the parsed
+ * values. See {@link #parseApproval} and {@link #parseCopiedApproval} for the valid {@link
+ * #footerLine} values.
+ */
+ @AutoValue
+ public abstract static class ParsedPatchSetApproval {
+
+ /** The original footer value, that this entity was parsed from. */
+ public abstract String footerLine();
+
+ public abstract boolean isRemoval();
+
+ /** Either <LABEL>=VOTE or <LABEL> for {@link #isRemoval}. */
+ public abstract String labelVote();
+
+ public abstract Optional<String> uuid();
+
+ public abstract Optional<String> accountIdent();
+
+ public abstract Optional<String> realAccountIdent();
+
+ public abstract Optional<String> tag();
+
+ public static Builder builder() {
+ return new AutoValue_ChangeNotesParseApprovalUtil_ParsedPatchSetApproval.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ abstract Builder footerLine(String labelLine);
+
+ abstract Builder isRemoval(boolean isRemoval);
+
+ abstract Builder labelVote(String labelVote);
+
+ abstract Builder uuid(Optional<String> uuid);
+
+ abstract Builder accountIdent(Optional<String> accountIdent);
+
+ abstract Builder realAccountIdent(Optional<String> realAccountIdent);
+
+ abstract Builder tag(Optional<String> tag);
+
+ abstract ParsedPatchSetApproval build();
+ }
+ }
+
+ /**
+ * Parses {@link ParsedPatchSetApproval} from {@link ChangeNoteFooters#FOOTER_LABEL} line.
+ *
+ * <p>Valid added approval footer examples:
+ *
+ * <ul>
+ * <li>Label: <LABEL>=VOTE
+ * <li>Label: <LABEL>=VOTE <Gerrit Account>
+ * <li>Label: <LABEL>=VOTE, <UUID>
+ * <li>Label: <LABEL>=VOTE, <UUID> <Gerrit Account>
+ * </ul>
+ *
+ * <p>Valid removed approval footer examples:
+ *
+ * <ul>
+ * <li>-<LABEL>
+ * <li>-<LABEL> <Gerrit Account>
+ * </ul>
+ *
+ * <p><UUID> is optional, since the approval might have been granted before {@link
+ * com.google.gerrit.entities.PatchSetApproval.UUID} was introduced.
+ *
+ * <p><Gerrit Account> is only persisted in cases, when the account, that granted the vote does
+ * not match the account, that issued {@link ChangeUpdate} (created this NoteDB commit).
+ */
+ public static ParsedPatchSetApproval parseApproval(String footerLine)
+ throws ConfigInvalidException {
+ try {
+ ParsedPatchSetApproval.Builder rawPatchSetApproval =
+ ParsedPatchSetApproval.builder().footerLine(footerLine);
+ String labelVoteStr;
+ boolean isRemoval = footerLine.startsWith("-");
+ rawPatchSetApproval.isRemoval(isRemoval);
+ int uuidStart = isRemoval ? -1 : footerLine.indexOf(", ");
+ int reviewerStart = footerLine.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
+ int labelStart = isRemoval ? 1 : 0;
+ checkFooter(!isRemoval || uuidStart == -1, FOOTER_LABEL, footerLine);
+
+ if (uuidStart != -1) {
+ String uuid =
+ footerLine.substring(
+ uuidStart + 2, reviewerStart > 0 ? reviewerStart : footerLine.length());
+ checkFooter(!Strings.isNullOrEmpty(uuid), FOOTER_LABEL, footerLine);
+ labelVoteStr = footerLine.substring(labelStart, uuidStart);
+ rawPatchSetApproval.uuid(Optional.of(uuid));
+ } else if (reviewerStart != -1) {
+ labelVoteStr = footerLine.substring(labelStart, reviewerStart);
+ } else {
+ labelVoteStr = footerLine.substring(labelStart);
+ }
+ rawPatchSetApproval.labelVote(labelVoteStr);
+
+ if (reviewerStart > 0) {
+ String ident = footerLine.substring(reviewerStart + 1);
+ rawPatchSetApproval.accountIdent(Optional.of(ident));
+ }
+ return rawPatchSetApproval.build();
+ } catch (StringIndexOutOfBoundsException ex) {
+ throw parseException(FOOTER_LABEL, footerLine, ex);
+ }
+ }
+
+ /**
+ * Parses copied {@link ParsedPatchSetApproval} from {@link ChangeNoteFooters#FOOTER_COPIED_LABEL}
+ * line.
+ *
+ * <p>Footer example: Copied-Label: <LABEL>=VOTE, <UUID> <Gerrit Account>,<Gerrit Real Account>
+ * :"<TAG>"
+ *
+ * <ul>
+ * <li>":<"TAG>"" is optional.
+ * <li><Gerrit Real Account> is also optional, if it was not set.
+ * <li><UUID> is optional, since the approval might have been granted before {@link
+ * com.google.gerrit.entities.PatchSetApproval.UUID} was introduced.
+ * <li>The label, vote, and the Gerrit account are mandatory (unlike FOOTER_LABEL where Gerrit
+ * Account is also optional since by default it's the committer).
+ * </ul>
+ *
+ * <p>Footer example for removal: Copied-Label: -<LABEL> <Gerrit Account>,<Gerrit Real Account>
+ *
+ * <ul>
+ * <li><Gerrit Real Account> is also optional, if it was not set.
+ * </ul>
+ */
+ public static ParsedPatchSetApproval parseCopiedApproval(String labelLine)
+ throws ConfigInvalidException {
+ try {
+ ParsedPatchSetApproval.Builder rawPatchSetApproval =
+ ParsedPatchSetApproval.builder().footerLine(labelLine);
+
+ boolean isRemoval = labelLine.startsWith("-");
+ rawPatchSetApproval.isRemoval(isRemoval);
+ int labelStart = isRemoval ? 1 : 0;
+ int uuidStart = isRemoval ? -1 : labelLine.indexOf(", ");
+ int tagStart = isRemoval ? -1 : labelLine.indexOf(":\"");
+
+ checkFooter(!isRemoval || uuidStart == -1, FOOTER_LABEL, labelLine);
+
+ // Weird tag that contains uuid delimiter. The uuid is actually not present.
+ if (tagStart != -1 && uuidStart > tagStart) {
+ uuidStart = -1;
+ }
+
+ int identitiesStart = labelLine.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
+ checkFooter(
+ identitiesStart != -1 && identitiesStart < labelLine.length(),
+ FOOTER_COPIED_LABEL,
+ labelLine);
+
+ String labelVoteStr =
+ labelLine.substring(labelStart, uuidStart != -1 ? uuidStart : identitiesStart);
+ rawPatchSetApproval.labelVote(labelVoteStr);
+ if (uuidStart != -1) {
+ String uuid = labelLine.substring(uuidStart + 2, identitiesStart);
+ checkFooter(!Strings.isNullOrEmpty(uuid), FOOTER_COPIED_LABEL, labelLine);
+ rawPatchSetApproval.uuid(Optional.of(uuid));
+ }
+ // The first account is the accountId, and second (if applicable) is the realAccountId.
+ List<String> identities =
+ Splitter.on(',')
+ .splitToList(
+ labelLine.substring(
+ identitiesStart + 1, tagStart == -1 ? labelLine.length() : tagStart));
+ checkFooter(identities.size() >= 1, FOOTER_COPIED_LABEL, labelLine);
+
+ rawPatchSetApproval.accountIdent(Optional.of(identities.get(0)));
+
+ if (identities.size() > 1) {
+ rawPatchSetApproval.realAccountIdent(Optional.of(identities.get(1)));
+ }
+
+ if (tagStart != -1) {
+ // tagStart+2 skips ":\"" to parse the actual tag. Tags are in brackets.
+ // line.length()-1 skips the last ".
+ String tag = labelLine.substring(tagStart + 2, labelLine.length() - 1);
+ rawPatchSetApproval.tag(Optional.of(tag));
+ }
+ return rawPatchSetApproval.build();
+ } catch (StringIndexOutOfBoundsException ex) {
+ throw parseException(FOOTER_COPIED_LABEL, labelLine, ex);
+ }
+ }
+
+ private static void checkFooter(boolean expr, FooterKey footer, String actual)
+ throws ConfigInvalidException {
+ if (!expr) {
+ throw parseException(footer, actual, /*cause=*/ null);
+ }
+ }
+
+ private static ConfigInvalidException parseException(
+ FooterKey footer, String actual, Throwable cause) {
+ return new ConfigInvalidException(
+ String.format("invalid %s: %s", footer.getName(), actual), cause);
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index f2a659d..77d1c8f 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -15,29 +15,29 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COPIED_LABEL;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PRIVATE;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REVERT_OF;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_STATUS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBJECT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMISSION_ID;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TOPIC;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;
@@ -80,8 +80,8 @@
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.account.externalids.ExternalIdCache;
-import com.google.gerrit.server.notedb.ChangeNoteUtil.ParsedPatchSetApproval;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
+import com.google.gerrit.server.notedb.ChangeNotesParseApprovalUtil.ParsedPatchSetApproval;
import com.google.gerrit.server.util.LabelVote;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -330,6 +330,7 @@
return result;
}
+ @Nullable
private PatchSet.Id buildCurrentPatchSetId() {
// currentPatchSets are in parse order, i.e. newest first. Pick the first
// patch set that was marked as current, excluding deleted patch sets.
@@ -583,6 +584,7 @@
return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
}
+ @Nullable
private String parseBranch(ChangeNotesCommit commit) throws ConfigInvalidException {
String branch = parseOneFooter(commit, FOOTER_BRANCH);
return branch != null ? RefNames.fullName(branch) : null;
@@ -610,6 +612,7 @@
return parseOneFooter(commit, FOOTER_TOPIC);
}
+ @Nullable
private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
throws ConfigInvalidException {
List<String> footerLines = commit.getFooterLineValues(footerKey);
@@ -630,6 +633,7 @@
return line;
}
+ @Nullable
private ObjectId parseRevision(ChangeNotesCommit commit) throws ConfigInvalidException {
String sha = parseOneFooter(commit, FOOTER_COMMIT);
if (sha == null) {
@@ -767,6 +771,7 @@
}
}
+ @Nullable
private Change.Status parseStatus(ChangeNotesCommit commit) throws ConfigInvalidException {
List<String> statusLines = commit.getFooterLineValues(FOOTER_STATUS);
if (statusLines.isEmpty()) {
@@ -805,6 +810,7 @@
return PatchSet.id(id, psId);
}
+ @Nullable
private PatchSetState parsePatchSetState(ChangeNotesCommit commit) throws ConfigInvalidException {
String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
int s = psIdLine.indexOf(' ');
@@ -936,7 +942,8 @@
/** Parses copied {@link PatchSetApproval}. */
private void parseCopiedApproval(PatchSet.Id psId, Instant ts, String line)
throws ConfigInvalidException {
- ParsedPatchSetApproval parsedPatchSetApproval = ChangeNoteUtil.parseCopiedApproval(line);
+ ParsedPatchSetApproval parsedPatchSetApproval =
+ ChangeNotesParseApprovalUtil.parseCopiedApproval(line);
checkFooter(
parsedPatchSetApproval.accountIdent().isPresent(),
FOOTER_COPIED_LABEL,
@@ -994,7 +1001,8 @@
throw parseException("patch set %s requires an identified user as uploader", psId.get());
}
PatchSetApproval.Builder psa;
- ParsedPatchSetApproval parsedPatchSetApproval = ChangeNoteUtil.parseApproval(line);
+ ParsedPatchSetApproval parsedPatchSetApproval =
+ ChangeNotesParseApprovalUtil.parseApproval(line);
if (line.startsWith("-")) {
psa = parseRemoveApproval(psId, accountId, realAccountId, ts, parsedPatchSetApproval);
} else {
@@ -1003,7 +1011,7 @@
bufferedApprovals.add(psa);
}
- /** Parses {@link PatchSetApproval} out of the {@link ChangeNoteUtil#FOOTER_LABEL} value. */
+ /** Parses {@link PatchSetApproval} out of the {@link ChangeNoteFooters#FOOTER_LABEL} value. */
private PatchSetApproval.Builder parseAddApproval(
PatchSet.Id psId,
Account.Id committerId,
@@ -1145,6 +1153,7 @@
}
}
+ @Nullable
private Account.Id parseIdent(ChangeNotesCommit commit) throws ConfigInvalidException {
// Check if the author name/email is the same as the committer name/email,
// i.e. was the server ident at the time this commit was made.
@@ -1230,6 +1239,7 @@
throw invalidFooter(FOOTER_WORK_IN_PROGRESS, raw);
}
+ @Nullable
private Change.Id parseRevertOf(ChangeNotesCommit commit) throws ConfigInvalidException {
String footer = parseOneFooter(commit, FOOTER_REVERT_OF);
if (footer == null) {
@@ -1243,7 +1253,7 @@
}
/**
- * Parses {@link ChangeNoteUtil#FOOTER_CHERRY_PICK_OF} of the commit.
+ * Parses {@link ChangeNoteFooters#FOOTER_CHERRY_PICK_OF} of the commit.
*
* @param commit the commit to parse.
* @return {@link Optional} value of the parsed footer or {@code null} if the footer is missing in
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 62c734b..5d43e28 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -18,29 +18,29 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COPIED_LABEL;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PRIVATE;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REVERT_OF;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_STATUS;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBJECT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMISSION_ID;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TOPIC;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Comparator.naturalOrder;
@@ -571,6 +571,7 @@
}
/** Returns the tree id for the updated tree */
+ @Nullable
private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
throws ConfigInvalidException, IOException {
if (submitRequirementResults == null && comments.isEmpty() && pushCert == null) {
diff --git a/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java b/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java
index 2f47107..e74af5b 100644
--- a/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java
+++ b/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java
@@ -16,6 +16,7 @@
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
@@ -27,7 +28,7 @@
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
-import java.time.temporal.TemporalAccessor;
+import java.util.Locale;
/**
* Adapter that reads/writes {@link Timestamp}s as ISO 8601 instant in UTC.
@@ -49,6 +50,16 @@
private static final DateTimeFormatter FALLBACK =
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
+ /**
+ * Fixed format to parse date/time in the "Feb 7, 2017 2:20:30 AM" format
+ *
+ * <p>Some old comments (created in Jan-Feb 2017) can be stored in legacy format, which can't be
+ * parsed with {@link #FALLBACK} formatter if the system/default locale has been changed. We will
+ * try to parse with a fixed format if {@link #FALLBACK} doesn't work.
+ */
+ private static final DateTimeFormatter FIXED_FORMAT_FALLBACK =
+ DateTimeFormatter.ofPattern("MMM d, yyyy h:mm:ss a").withLocale(Locale.US);
+
@Override
public void write(JsonWriter out, Timestamp ts) throws IOException {
Timestamp truncated = new Timestamp(ts.getTime() / 1000 * 1000);
@@ -58,12 +69,26 @@
@Override
public Timestamp read(JsonReader in) throws IOException {
String str = in.nextString();
- TemporalAccessor ta;
try {
- ta = ISO_INSTANT.parse(str);
+ return Timestamp.from(Instant.from(ISO_INSTANT.parse(str)));
} catch (DateTimeParseException e) {
- ta = LocalDateTime.from(FALLBACK.parse(str)).atZone(ZoneId.systemDefault());
+ try {
+ return parseDateTimeWithDefaultLocaleFormat(str);
+ } catch (DateTimeParseException e2) {
+ return parseDateTimeWithFixedFormat(str);
+ }
}
- return Timestamp.from(Instant.from(ta));
+ }
+
+ public static Timestamp parseDateTimeWithDefaultLocaleFormat(String str) {
+ return Timestamp.from(
+ Instant.from(LocalDateTime.from(FALLBACK.parse(str)).atZone(ZoneId.systemDefault())));
+ }
+
+ @VisibleForTesting
+ public static Timestamp parseDateTimeWithFixedFormat(String str) {
+ return Timestamp.from(
+ Instant.from(
+ LocalDateTime.from(FIXED_FORMAT_FALLBACK.parse(str)).atZone(ZoneId.systemDefault())));
}
}
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index da20475..a67dc07 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -15,12 +15,12 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_PATTERN;
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_REGEX;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -893,7 +893,7 @@
commitMessageRange.get().subjectEnd());
Optional<String> fixedChangeMessage = Optional.empty();
String originalChangeMessage = null;
- if (commitMessageRange.isPresent() && commitMessageRange.get().hasChangeMessage()) {
+ if (commitMessageRange.get().hasChangeMessage()) {
originalChangeMessage =
RawParseUtils.decode(
enc,
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 5d8f57f..bdfe378 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -141,6 +141,7 @@
return args.allUsers;
}
+ @Nullable
@VisibleForTesting
NoteMap getNoteMap() {
return revisionNoteMap != null ? revisionNoteMap.noteMap : null;
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index ad1f4c5..0939ada 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -365,6 +365,7 @@
cu -> cu.getAttentionSetUpdates().stream()));
}
+ @Nullable
private BatchRefUpdate execute(OpenRepo or, boolean dryrun, @Nullable PushCertificate pushCert)
throws IOException {
if (or == null || or.cmds.isEmpty()) {
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 64bf430..2ad89b2 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -111,6 +112,7 @@
* Returns the name of the REST API handler that is in the stack trace of the caller of this
* method.
*/
+ @Nullable
static String guessRestApiHandler() {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
int i = findRestApiServlet(trace);
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index 7f067f5..e1e6305 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -19,6 +19,7 @@
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -100,6 +101,7 @@
put.add(c);
}
+ @Nullable
private CommitBuilder storeCommentsInNotes(
RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
throws ConfigInvalidException, IOException {
diff --git a/java/com/google/gerrit/server/patch/BaseCommitUtil.java b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
index 56a01b9..9a103cd 100644
--- a/java/com/google/gerrit/server/patch/BaseCommitUtil.java
+++ b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
@@ -91,6 +91,7 @@
* @return Returns the parent commit of the commit represented by the commitId parameter. Note
* that auto-merge is not supported for commits having more than two parents.
*/
+ @Nullable
RevObject getParentCommit(
Repository repo,
ObjectInserter ins,
diff --git a/java/com/google/gerrit/server/patch/FilePathAdapter.java b/java/com/google/gerrit/server/patch/FilePathAdapter.java
index 2c98f1a..d0b7ac6 100644
--- a/java/com/google/gerrit/server/patch/FilePathAdapter.java
+++ b/java/com/google/gerrit/server/patch/FilePathAdapter.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.patch;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Patch.ChangeType;
import java.util.Optional;
@@ -30,6 +31,7 @@
/**
* Converts the old file path of the new diff cache output to the old diff cache representation.
*/
+ @Nullable
public static String getOldPath(Optional<String> oldName, ChangeType changeType) {
switch (changeType) {
case DELETED:
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 33300e3..1612925 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -19,6 +19,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.entities.FixReplacement;
@@ -209,6 +210,7 @@
}
}
+ @Nullable
private static String oldName(PatchFileChange entry) {
switch (entry.getChangeType()) {
case ADDED:
@@ -224,6 +226,7 @@
}
}
+ @Nullable
private static String newName(PatchFileChange entry) {
switch (entry.getChangeType()) {
case DELETED:
@@ -412,6 +415,7 @@
treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode);
}
+ @Nullable
private TreeWalk find(ObjectReader reader, String path, ObjectId within) throws IOException {
if (path == null || within == null) {
return null;
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index 3e4e72d..d1bda5c 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -97,7 +97,7 @@
persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
.maximumWeight(10 << 20)
.weigher(FileDiffWeigher.class)
- .version(8)
+ .version(9)
.keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
.valueSerializer(FileDiffOutput.Serializer.INSTANCE)
.loader(FileDiffLoader.class);
@@ -443,6 +443,8 @@
.patchType(mainGitDiff.patchType())
.oldPath(mainGitDiff.oldPath())
.newPath(mainGitDiff.newPath())
+ .oldMode(mainGitDiff.oldMode())
+ .newMode(mainGitDiff.newMode())
.headerLines(FileHeaderUtil.getHeaderLines(mainGitDiff.fileHeader()))
.edits(asTaggedEdits(mainGitDiff.edits(), rebaseEdits))
.size(newSize)
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
index 31fe77a..9286f47 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
@@ -17,9 +17,12 @@
import static com.google.gerrit.server.patch.DiffUtil.stringSize;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.Patch.FileMode;
import com.google.gerrit.entities.Patch.PatchType;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.proto.Cache.FileDiffOutputProto;
@@ -61,6 +64,18 @@
*/
public abstract Optional<String> newPath();
+ /**
+ * The file mode of the old file at the old git tree diff identified by {@link #oldCommitId()}
+ * ()}.
+ */
+ public abstract Optional<Patch.FileMode> oldMode();
+
+ /**
+ * The file mode of the new file at the new git tree diff identified by {@link #newCommitId()}
+ * ()}.
+ */
+ public abstract Optional<Patch.FileMode> newMode();
+
/** The change type of the underlying file, e.g. added, deleted, renamed, etc... */
public abstract Patch.ChangeType changeType();
@@ -201,6 +216,10 @@
public abstract Builder newPath(Optional<String> value);
+ public abstract Builder oldMode(Optional<Patch.FileMode> oldMode);
+
+ public abstract Builder newMode(Optional<Patch.FileMode> newMode);
+
public abstract Builder changeType(ChangeType value);
public abstract Builder patchType(Optional<PatchType> value);
@@ -221,6 +240,9 @@
public enum Serializer implements CacheSerializer<FileDiffOutput> {
INSTANCE;
+ private static final Converter<String, FileMode> FILE_MODE_CONVERTER =
+ Enums.stringConverter(Patch.FileMode.class);
+
private static final FieldDescriptor OLD_PATH_DESCRIPTOR =
FileDiffOutputProto.getDescriptor().findFieldByNumber(1);
@@ -233,6 +255,12 @@
private static final FieldDescriptor NEGATIVE_DESCRIPTOR =
FileDiffOutputProto.getDescriptor().findFieldByNumber(12);
+ private static final FieldDescriptor OLD_MODE_DESCRIPTOR =
+ FileDiffOutputProto.getDescriptor().findFieldByNumber(13);
+
+ private static final FieldDescriptor NEW_MODE_DESCRIPTOR =
+ FileDiffOutputProto.getDescriptor().findFieldByNumber(14);
+
@Override
public byte[] serialize(FileDiffOutput fileDiff) {
ObjectIdConverter idConverter = ObjectIdConverter.create();
@@ -277,6 +305,13 @@
builder.setNegative(fileDiff.negative().get());
}
+ if (fileDiff.oldMode().isPresent()) {
+ builder.setOldMode(FILE_MODE_CONVERTER.reverse().convert(fileDiff.oldMode().get()));
+ }
+ if (fileDiff.newMode().isPresent()) {
+ builder.setNewMode(FILE_MODE_CONVERTER.reverse().convert(fileDiff.newMode().get()));
+ }
+
return Protos.toByteArray(builder.build());
}
@@ -318,6 +353,12 @@
if (proto.hasField(NEGATIVE_DESCRIPTOR)) {
builder.negative(Optional.of(proto.getNegative()));
}
+ if (proto.hasField(OLD_MODE_DESCRIPTOR)) {
+ builder.oldMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getOldMode())));
+ }
+ if (proto.hasField(NEW_MODE_DESCRIPTOR)) {
+ builder.newMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getNewMode())));
+ }
return builder.build();
}
}
diff --git a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
index 12a7841..0e5ff48 100644
--- a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
+++ b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.permissions;
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
@@ -27,9 +25,9 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.change.ChangeData;
import java.io.IOException;
+import java.util.HashMap;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jgit.lib.Repository;
@@ -72,15 +70,18 @@
ImmutableSet<Change.Id> changes) {
Stream<ChangeData> changeDatas;
if (changes.size() < CHANGE_LIMIT_FOR_DIRECT_FILTERING) {
+ logger.atFine().log("Loading changes one by one for project %s", projectName);
changeDatas = loadChangeDatasOneByOne(changes, changeDataFactory, projectName);
} else if (searchingChangeCache != null) {
+ logger.atFine().log("Loading changes from SearchingChangeCache for project %s", projectName);
changeDatas = searchingChangeCache.getChangeData(projectName);
} else {
+ logger.atFine().log("Loading changes from all refs for project %s", projectName);
changeDatas =
scanRepoForChangeDatas(changeNotesFactory, changeDataFactory, repository, projectName);
}
-
- return changeDatas
+ HashMap<Change.Id, ChangeData> result = new HashMap<>();
+ changeDatas
.filter(cd -> changes.contains(cd.getId()))
.filter(
cd -> {
@@ -90,7 +91,16 @@
throw new StorageException(e);
}
})
- .collect(toImmutableMap(ChangeData::getId, Function.identity()));
+ .forEach(
+ cd -> {
+ if (result.containsKey(cd.getId())) {
+ logger.atWarning().log(
+ "Duplicate change datas for the repo %s: [%s, %s]",
+ projectName, cd, result.get(cd.getId()));
+ }
+ result.put(cd.getId(), cd);
+ });
+ return ImmutableMap.copyOf(result);
}
/** Get a stream of changes by loading them individually. */
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index e4fa1c4..c235012 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -21,6 +21,7 @@
import static com.google.gerrit.server.util.MagicBranch.NEW_CHANGE;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
@@ -269,6 +270,7 @@
return false;
}
+ @Nullable
private Boolean canPerform(String permissionName, AccessSection section, Permission permission) {
for (PermissionRule rule : permission.getRules()) {
if (rule.isBlock() || rule.isDeny() || !match(rule)) {
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 478ba5c..ba292e6 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.PermissionRange;
@@ -177,6 +178,7 @@
}
/** The range of permitted values associated with a label permission. */
+ @Nullable
PermissionRange getRange(String permission, boolean isChangeOwner) {
if (Permission.hasRange(permission)) {
return toRange(permission, isChangeOwner);
diff --git a/java/com/google/gerrit/server/plugins/JarScanner.java b/java/com/google/gerrit/server/plugins/JarScanner.java
index e119bf1..122e3f4 100644
--- a/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -24,6 +24,7 @@
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
@@ -212,6 +213,7 @@
this.superName = superName;
}
+ @Nullable
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (!visible) {
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 8d17d85..3263636 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -27,6 +27,7 @@
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -713,6 +714,7 @@
return Iterables.filter(paths, p -> !p.getFileName().toString().endsWith(".disabled"));
}
+ @Nullable
public String getGerritPluginName(Path srcPath) {
String fileName = srcPath.getFileName().toString();
if (isUiPlugin(fileName)) {
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 320b618..af948b0 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -110,6 +110,7 @@
}
}
+ @Nullable
@SuppressWarnings("unchecked")
protected static Class<? extends Module> load(@Nullable String name, ClassLoader pluginLoader)
throws ClassNotFoundException {
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index c1b7b86..196873f 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -56,6 +57,7 @@
return projectName;
}
+ @Nullable
public String getProjectName() {
return projectName != null ? projectName.get() : null;
}
diff --git a/java/com/google/gerrit/server/project/GroupList.java b/java/com/google/gerrit/server/project/GroupList.java
index 98dc44a..1b0ba97 100644
--- a/java/com/google/gerrit/server/project/GroupList.java
+++ b/java/com/google/gerrit/server/project/GroupList.java
@@ -126,6 +126,7 @@
byUUID.put(uuid, reference);
}
+ @Nullable
public String asText() {
if (byUUID.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
index 235eb34..f46c2b1 100644
--- a/java/com/google/gerrit/server/project/LabelDefinitionJson.java
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -16,6 +16,7 @@
import static java.util.stream.Collectors.toMap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.Project;
@@ -39,8 +40,9 @@
return label;
}
+ @Nullable
private static Boolean toBoolean(boolean v) {
- return v ? v : null;
+ return v ? Boolean.TRUE : null;
}
private LabelDefinitionJson() {}
diff --git a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
index df2e1cf..85a3ab9 100644
--- a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
@@ -92,6 +92,7 @@
}
@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 67c031e..7fdd113 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -24,6 +24,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.Futures;
@@ -54,6 +55,7 @@
import com.google.gerrit.server.config.AllProjectsConfigProvider;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
@@ -67,6 +69,7 @@
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.time.Duration;
+import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -75,6 +78,7 @@
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -158,6 +162,7 @@
};
}
+ private final Config config;
private final AllProjectsName allProjectsName;
private final AllUsersName allUsersName;
private final LoadingCache<Project.NameKey, CachedProjectConfig> inMemoryProjectCache;
@@ -169,13 +174,15 @@
@Inject
ProjectCacheImpl(
- final AllProjectsName allProjectsName,
- final AllUsersName allUsersName,
+ @GerritServerConfig Config config,
+ AllProjectsName allProjectsName,
+ AllUsersName allUsersName,
@Named(CACHE_NAME) LoadingCache<Project.NameKey, CachedProjectConfig> inMemoryProjectCache,
@Named(CACHE_LIST) LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list,
Provider<ProjectIndexer> indexer,
MetricMaker metricMaker,
ProjectState.Factory projectStateFactory) {
+ this.config = config;
this.allProjectsName = allProjectsName;
this.allUsersName = allUsersName;
this.inMemoryProjectCache = inMemoryProjectCache;
@@ -293,13 +300,16 @@
@Override
public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
- return 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)
+ 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());
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 47b0a53..2dd7970 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -977,6 +977,7 @@
Map<String, String> lowerNames = new HashMap<>();
submitRequirementSections = new LinkedHashMap<>();
for (String name : rc.getSubsections(SUBMIT_REQUIREMENT)) {
+ checkDuplicateSrDefinition(rc, name);
String lower = name.toLowerCase();
if (lowerNames.containsKey(lower)) {
error(
@@ -1034,6 +1035,40 @@
}
}
+ private void checkDuplicateSrDefinition(Config rc, String srName) {
+ if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_DESCRIPTION).length > 1) {
+ error(
+ String.format(
+ "Multiple definitions of %s for submit requirement '%s'",
+ KEY_SR_DESCRIPTION, srName));
+ }
+ if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_APPLICABILITY_EXPRESSION).length > 1) {
+ error(
+ String.format(
+ "Multiple definitions of %s for submit requirement '%s'",
+ KEY_SR_APPLICABILITY_EXPRESSION, srName));
+ }
+ if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_SUBMITTABILITY_EXPRESSION).length > 1) {
+ error(
+ String.format(
+ "Multiple definitions of %s for submit requirement '%s'",
+ KEY_SR_SUBMITTABILITY_EXPRESSION, srName));
+ }
+ if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_OVERRIDE_EXPRESSION).length > 1) {
+ error(
+ String.format(
+ "Multiple definitions of %s for submit requirement '%s'",
+ KEY_SR_OVERRIDE_EXPRESSION, srName));
+ }
+ if (rc.getStringList(SUBMIT_REQUIREMENT, srName, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS).length
+ > 1) {
+ error(
+ String.format(
+ "Multiple definitions of %s for submit requirement '%s'",
+ KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, srName));
+ }
+ }
+
/**
* Report unsupported submit requirement parameters as errors.
*
@@ -1168,6 +1203,7 @@
return false;
}
+ @Nullable
private List<String> getStringListOrNull(
Config rc, String section, String subSection, String name) {
String[] ac = rc.getStringList(section, subSection, name);
@@ -1337,6 +1373,7 @@
return true;
}
+ @Nullable
public static String validMaxObjectSizeLimit(String value) throws ConfigInvalidException {
if (value == null) {
return null;
diff --git a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
index ccb5651..929399a 100644
--- a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
+++ b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -18,6 +18,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.config.AllProjectsName;
import java.util.Iterator;
@@ -63,6 +64,7 @@
return n;
}
+ @Nullable
private ProjectState computeNext(ProjectState n) {
Project.NameKey parentName = n.getProject().getParent();
if (parentName != null && visit(parentName)) {
diff --git a/java/com/google/gerrit/server/project/SectionMatcher.java b/java/com/google/gerrit/server/project/SectionMatcher.java
index 3d7175f..eaebab2 100644
--- a/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.CurrentUser;
@@ -25,6 +26,7 @@
* of which sections are relevant to any given input reference.
*/
public class SectionMatcher extends RefPatternMatcher {
+ @Nullable
static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
String ref = section.getName();
if (AccessSection.isValidRefSectionName(ref)) {
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index ed950c8..e31411c 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -14,9 +14,12 @@
package com.google.gerrit.server.query.account;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.NotSignedInException;
import com.google.gerrit.exceptions.StorageException;
@@ -37,6 +40,8 @@
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.account.AccountPredicates.AccountPredicate;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
@@ -61,6 +66,7 @@
public static class Arguments {
final ChangeFinder changeFinder;
+ final ChangeData.Factory changeDataFactory;
final PermissionBackend permissionBackend;
private final Provider<CurrentUser> self;
@@ -71,9 +77,11 @@
Provider<CurrentUser> self,
AccountIndexCollection indexes,
ChangeFinder changeFinder,
+ ChangeData.Factory changeDataFactory,
PermissionBackend permissionBackend) {
this.self = self;
this.indexes = indexes;
+ this.changeDataFactory = changeDataFactory;
this.changeFinder = changeFinder;
this.permissionBackend = permissionBackend;
}
@@ -98,6 +106,7 @@
}
}
+ @Nullable
Schema<AccountState> schema() {
Index<?, AccountState> index = indexes != null ? indexes.getSearchIndex() : null;
return index != null ? index.getSchema() : null;
@@ -119,7 +128,17 @@
if (!changeNotes.isPresent()) {
throw error(String.format("change %s not found", change));
}
-
+ if (changeNotes.get().getChange().isPrivate()) {
+ Account.Id caller = self();
+ ChangeData cd = args.changeDataFactory.create(changeNotes.get());
+ Account.Id owner = cd.change().getOwner();
+ ImmutableSet<Account.Id> reviewersAndCC = cd.reviewers().all();
+ if (!(caller.equals(owner) || reviewersAndCC.contains(caller))) {
+ throw error(String.format("change %s not found", change));
+ }
+ return orAccountPredicate(
+ ImmutableList.<Account.Id>builder().add(owner).addAll(reviewersAndCC).build());
+ }
if (!args.permissionBackend
.user(args.getUser())
.change(changeNotes.get())
@@ -229,4 +248,14 @@
return false;
}
}
+
+ /** Creates an OR predicate of the account IDs of the {@code accounts} parameter. */
+ private Predicate<AccountState> orAccountPredicate(ImmutableList<Account.Id> accounts) {
+ Predicate<AccountState> result =
+ AccountPredicate.or(AccountPredicates.id(args.schema(), accounts.get(0)));
+ for (int i = 1; i < accounts.size(); i += 1) {
+ result = AccountPredicate.or(result, AccountPredicates.id(args.schema(), accounts.get(i)));
+ }
+ return result;
+ }
}
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 98a12d5..fa1758a 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Project;
import com.google.gerrit.index.IndexConfig;
@@ -71,6 +72,7 @@
return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
}
+ @Nullable
@UsedAt(UsedAt.Project.COLLABNET)
public AccountState oneByExternalId(ExternalId.Key externalId) {
List<AccountState> accountStates = byExternalId(externalId);
diff --git a/java/com/google/gerrit/server/query/change/AfterPredicate.java b/java/com/google/gerrit/server/query/change/AfterPredicate.java
index 2514989..d3e3477 100644
--- a/java/com/google/gerrit/server/query/change/AfterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.QueryParseException;
import java.sql.Timestamp;
import java.time.Instant;
@@ -26,7 +26,7 @@
public class AfterPredicate extends TimestampRangeChangePredicate {
protected final Instant cut;
- public AfterPredicate(FieldDef<ChangeData, Timestamp> def, String name, String value)
+ public AfterPredicate(SchemaField<ChangeData, Timestamp> def, String name, String value)
throws QueryParseException {
super(def, name, value);
cut = parse(value);
diff --git a/java/com/google/gerrit/server/query/change/BeforePredicate.java b/java/com/google/gerrit/server/query/change/BeforePredicate.java
index 5d682fb..e9ddbff 100644
--- a/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.QueryParseException;
import java.sql.Timestamp;
import java.time.Instant;
@@ -26,7 +26,7 @@
public class BeforePredicate extends TimestampRangeChangePredicate {
protected final Instant cut;
- public BeforePredicate(FieldDef<ChangeData, Timestamp> def, String name, String value)
+ public BeforePredicate(SchemaField<ChangeData, Timestamp> def, String name, String value)
throws QueryParseException {
super(def, name, value);
cut = parse(value);
diff --git a/java/com/google/gerrit/server/query/change/BooleanPredicate.java b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
index 6ca3acc..d6df7e0 100644
--- a/java/com/google/gerrit/server/query/change/BooleanPredicate.java
+++ b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -14,10 +14,10 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
public class BooleanPredicate extends ChangeIndexPredicate {
- public BooleanPredicate(FieldDef<ChangeData, String> field) {
+ public BooleanPredicate(SchemaField<ChangeData, String> field) {
super(field, "1");
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 8ab9786..ec1fcad 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -74,7 +74,6 @@
import com.google.gerrit.server.change.MergeabilityCache;
import com.google.gerrit.server.change.PureRevert;
import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtilFactory;
@@ -263,65 +262,15 @@
* <p>Attempting to lazy load data will fail with NPEs. Callers may consider manually setting
* fields that can be set.
*
- * @param project project name
* @param id change ID
- * @param currentPatchSetId current patchset number
- * @param commitId commit SHA1 of the current patchset
* @return instance for testing.
*/
public static ChangeData createForTest(
Project.NameKey project, Change.Id id, int currentPatchSetId, ObjectId commitId) {
- return createForTest(project, id, currentPatchSetId, commitId, null, null, null);
- }
-
- /**
- * Create an instance for testing only.
- *
- * <p>Attempting to lazy load data will fail with NPEs. Callers may consider manually setting
- * fields that can be set.
- *
- * @param project project name
- * @param id change ID
- * @param currentPatchSetId current patchset number
- * @param commitId commit SHA1 of the current patchset
- * @param serverId Gerrit server id
- * @param virtualIdAlgo algorithm for virtualising the Change number
- * @param changeNotes notes associated with the Change
- * @return instance for testing.
- */
- public static ChangeData createForTest(
- Project.NameKey project,
- Change.Id id,
- int currentPatchSetId,
- ObjectId commitId,
- @Nullable String serverId,
- @Nullable ChangeNumberVirtualIdAlgorithm virtualIdAlgo,
- @Nullable ChangeNotes changeNotes) {
ChangeData cd =
new ChangeData(
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- serverId,
- virtualIdAlgo,
- project,
- id,
- null,
- changeNotes);
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, project, id, null, null);
cd.currentPatchSet =
PatchSet.builder()
.id(PatchSet.id(id, currentPatchSetId))
@@ -412,9 +361,6 @@
private Optional<Instant> mergedOn;
private ImmutableSetMultimap<NameKey, RefState> refStates;
private ImmutableList<byte[]> refStatePatterns;
- private String gerritServerId;
- private String changeServerId;
- private ChangeNumberVirtualIdAlgorithm virtualIdFunc;
@Inject
private ChangeData(
@@ -435,8 +381,6 @@
SubmitRequirementsEvaluator submitRequirementsEvaluator,
SubmitRequirementsUtil submitRequirementsUtil,
SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
- @GerritServerId String gerritServerId,
- ChangeNumberVirtualIdAlgorithm virtualIdFunc,
@Assisted Project.NameKey project,
@Assisted Change.Id id,
@Assisted @Nullable Change change,
@@ -464,10 +408,6 @@
this.change = change;
this.notes = notes;
-
- this.changeServerId = notes == null ? null : notes.getServerId();
- this.gerritServerId = gerritServerId;
- this.virtualIdFunc = virtualIdFunc;
}
/**
@@ -588,14 +528,6 @@
return legacyId;
}
- public Change.Id getVirtualId() {
- if (virtualIdFunc == null || changeServerId == null || changeServerId.equals(gerritServerId)) {
- return legacyId;
- }
-
- return Change.id(virtualIdFunc.apply(changeServerId, legacyId.get()));
- }
-
public Project.NameKey project() {
return project;
}
@@ -626,7 +558,6 @@
throw new StorageException("Unable to load change " + legacyId, e);
}
change = notes.getChange();
- changeServerId = notes.getServerId();
setPatchSets(null);
return change;
}
@@ -649,6 +580,7 @@
return notes;
}
+ @Nullable
public PatchSet currentPatchSet() {
if (currentPatchSet == null) {
Change c = change();
@@ -693,6 +625,7 @@
currentApprovals = approvals;
}
+ @Nullable
public String commitMessage() {
if (commitMessage == null) {
if (!loadCommitData()) {
@@ -716,6 +649,7 @@
return trackingFooters.extract(commitFooters());
}
+ @Nullable
public PersonIdent getAuthor() {
if (author == null) {
if (!loadCommitData()) {
@@ -725,6 +659,7 @@
return author;
}
+ @Nullable
public PersonIdent getCommitter() {
if (committer == null) {
if (!loadCommitData()) {
@@ -820,6 +755,7 @@
}
/** Returns patch with the given ID, or null if it does not exist. */
+ @Nullable
public PatchSet patchSet(PatchSet.Id psId) {
if (currentPatchSet != null && currentPatchSet.id().equals(psId)) {
return currentPatchSet;
@@ -961,6 +897,7 @@
return robotComments;
}
+ @Nullable
public Integer unresolvedCommentCount() {
if (unresolvedCommentCount == null) {
if (!lazyload()) {
@@ -983,6 +920,7 @@
this.unresolvedCommentCount = count;
}
+ @Nullable
public Integer totalCommentCount() {
if (totalCommentCount == null) {
if (!lazyload()) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexCardinalPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexCardinalPredicate.java
index 6540d80..e39b3e2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexCardinalPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexCardinalPredicate.java
@@ -14,20 +14,20 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.HasCardinality;
public class ChangeIndexCardinalPredicate extends ChangeIndexPredicate implements HasCardinality {
protected final int cardinality;
protected ChangeIndexCardinalPredicate(
- FieldDef<ChangeData, ?> def, String value, int cardinality) {
+ SchemaField<ChangeData, ?> def, String value, int cardinality) {
super(def, value);
this.cardinality = cardinality;
}
protected ChangeIndexCardinalPredicate(
- FieldDef<ChangeData, ?> def, String name, String value, int cardinality) {
+ SchemaField<ChangeData, ?> def, String name, String value, int cardinality) {
super(def, name, value);
this.cardinality = cardinality;
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPostFilterPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPostFilterPredicate.java
index d86d366..c69f021 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPostFilterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPostFilterPredicate.java
@@ -14,18 +14,19 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
/**
* Predicate that is mapped to a field in the change index, with additional filtering done in the
* {@code match} method.
*/
public abstract class ChangeIndexPostFilterPredicate extends ChangeIndexPredicate {
- protected ChangeIndexPostFilterPredicate(FieldDef<ChangeData, ?> def, String value) {
+ protected ChangeIndexPostFilterPredicate(SchemaField<ChangeData, ?> def, String value) {
super(def, value);
}
- protected ChangeIndexPostFilterPredicate(FieldDef<ChangeData, ?> def, String name, String value) {
+ protected ChangeIndexPostFilterPredicate(
+ SchemaField<ChangeData, ?> def, String name, String value) {
super(def, name, value);
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index ccd4109..a897a8d 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Predicate;
@@ -32,11 +32,11 @@
return ChangeStatusPredicate.NONE;
}
- protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String value) {
+ protected ChangeIndexPredicate(SchemaField<ChangeData, ?> def, String value) {
super(def, value);
}
- protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String name, String value) {
+ protected ChangeIndexPredicate(SchemaField<ChangeData, ?> def, String name, String value) {
super(def, name, value);
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeNumberBitmapMaskAlgorithm.java b/java/com/google/gerrit/server/query/change/ChangeNumberBitmapMaskAlgorithm.java
deleted file mode 100644
index 726a376..0000000
--- a/java/com/google/gerrit/server/query/change/ChangeNumberBitmapMaskAlgorithm.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2022 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.change;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.server.config.GerritImportedServerIds;
-import com.google.inject.Inject;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-/**
- * Dictionary-based encoding algorithm for combining a serverId/legacyChangeNum into a virtual
- * numeric id
- *
- * <p>TODO: To be reverted on master and stable-3.8
- */
-@Singleton
-public class ChangeNumberBitmapMaskAlgorithm implements ChangeNumberVirtualIdAlgorithm {
- /*
- * Bit-wise masks for representing the Change's VirtualId as combination of ServerId + ChangeNum:
- */
- private static final int CHANGE_NUM_BIT_LEN = 28; // Allows up to 268M changes
- private static final int LEGACY_ID_BIT_MASK = (1 << CHANGE_NUM_BIT_LEN) - 1;
- private static final int SERVER_ID_BIT_LEN =
- Integer.BYTES * 8 - CHANGE_NUM_BIT_LEN; // Allows up to 64 ServerIds
-
- private final ImmutableMap<String, Integer> serverIdCodes;
-
- @Inject
- public ChangeNumberBitmapMaskAlgorithm(
- @GerritImportedServerIds ImmutableList<String> importedServerIds) {
- if (importedServerIds.size() >= 1 << SERVER_ID_BIT_LEN) {
- throw new ProvisionException(
- String.format(
- "Too many imported GerritServerIds (%d) to fit into the Change virtual id",
- importedServerIds.size()));
- }
- ImmutableMap.Builder<String, Integer> serverIdCodesBuilder = new ImmutableMap.Builder<>();
- for (int i = 0; i < importedServerIds.size(); i++) {
- serverIdCodesBuilder.put(importedServerIds.get(i), i + 1);
- }
-
- serverIdCodes = serverIdCodesBuilder.build();
- }
-
- @Override
- public int apply(String changeServerId, int changeNum) {
- if ((changeNum & LEGACY_ID_BIT_MASK) != changeNum) {
- throw new IllegalArgumentException(
- String.format(
- "Change number %d is too large to be converted into a virtual id", changeNum));
- }
-
- Integer encodedServerId = serverIdCodes.get(changeServerId);
- if (encodedServerId == null) {
- throw new IllegalArgumentException(
- String.format("ServerId %s is not part of the GerritImportedServerIds", changeServerId));
- }
- int virtualId = (changeNum & LEGACY_ID_BIT_MASK) | (encodedServerId << CHANGE_NUM_BIT_LEN);
-
- return virtualId;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangeNumberVirtualIdAlgorithm.java b/java/com/google/gerrit/server/query/change/ChangeNumberVirtualIdAlgorithm.java
deleted file mode 100644
index ab21705..0000000
--- a/java/com/google/gerrit/server/query/change/ChangeNumberVirtualIdAlgorithm.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2022 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.change;
-
-import com.google.inject.ImplementedBy;
-
-/**
- * Algorithm for encoding a serverId/legacyChangeNum into a virtual numeric id
- *
- * <p>TODO: To be reverted on master and stable-3.8
- */
-@ImplementedBy(ChangeNumberBitmapMaskAlgorithm.class)
-public interface ChangeNumberVirtualIdAlgorithm {
-
- /**
- * Convert a serverId/legacyChangeNum tuple into a virtual numeric id
- *
- * @param serverId Gerrit serverId
- * @param legacyChangeNum legacy change number
- * @return virtual id which combines serverId and legacyChangeNum together
- */
- int apply(String serverId, int legacyChangeNum);
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 5f9abc3..70f241c 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -50,7 +50,7 @@
* com.google.gerrit.entities.Account.Id}.
*/
public static Predicate<ChangeData> assignee(Account.Id id) {
- return new ChangeIndexPredicate(ChangeField.ASSIGNEE, id.toString());
+ return new ChangeIndexPredicate(ChangeField.ASSIGNEE_SPEC, id.toString());
}
/**
@@ -136,7 +136,7 @@
* com.google.gerrit.entities.Account.Id}.
*/
public static Predicate<ChangeData> owner(Account.Id id) {
- return new ChangeIndexCardinalPredicate(ChangeField.OWNER, id.toString(), 5000);
+ return new ChangeIndexCardinalPredicate(ChangeField.OWNER_SPEC, id.toString(), 5000);
}
/**
@@ -144,7 +144,7 @@
* provided {@link com.google.gerrit.entities.Account.Id}.
*/
public static Predicate<ChangeData> uploader(Account.Id id) {
- return new ChangeIndexPredicate(ChangeField.UPLOADER, id.toString());
+ return new ChangeIndexPredicate(ChangeField.UPLOADER_SPEC, id.toString());
}
/**
@@ -170,12 +170,12 @@
* com.google.gerrit.entities.Project.NameKey}.
*/
public static Predicate<ChangeData> project(Project.NameKey id) {
- return new ChangeIndexCardinalPredicate(ChangeField.PROJECT, id.get(), 1_000_000);
+ return new ChangeIndexCardinalPredicate(ChangeField.PROJECT_SPEC, id.get(), 1_000_000);
}
/** Returns a predicate that matches changes targeted at the provided {@code refName}. */
public static Predicate<ChangeData> ref(String refName) {
- return new ChangeIndexCardinalPredicate(ChangeField.REF, refName, 10_000);
+ return new ChangeIndexCardinalPredicate(ChangeField.REF_SPEC, refName, 10_000);
}
/** Returns a predicate that matches changes in the provided {@code topic}. */
@@ -195,19 +195,19 @@
/** Returns a predicate that matches changes submitted in the provided {@code changeSet}. */
public static Predicate<ChangeData> submissionId(String changeSet) {
- return new ChangeIndexPredicate(ChangeField.SUBMISSIONID, changeSet);
+ return new ChangeIndexPredicate(ChangeField.SUBMISSIONID_SPEC, changeSet);
}
/** Returns a predicate that matches changes that modified the provided {@code path}. */
public static Predicate<ChangeData> path(String path) {
- return new ChangeIndexPredicate(ChangeField.PATH, path);
+ return new ChangeIndexPredicate(ChangeField.PATH_SPEC, path);
}
/** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
public static Predicate<ChangeData> hashtag(String hashtag) {
// Use toLowerCase without locale to match behavior in ChangeField.
return new ChangeIndexPredicate(
- ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+ ChangeField.HASHTAG_SPEC, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
}
/** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
@@ -229,10 +229,10 @@
/** Returns a predicate that matches changes that modified the provided {@code file}. */
public static Predicate<ChangeData> file(ChangeQueryBuilder.Arguments args, String file) {
Predicate<ChangeData> eqPath = path(file);
- if (!args.getSchema().hasField(ChangeField.FILE_PART)) {
+ if (!args.getSchema().hasField(ChangeField.FILE_PART_SPEC)) {
return eqPath;
}
- return Predicate.or(eqPath, new ChangeIndexPredicate(ChangeField.FILE_PART, file));
+ return Predicate.or(eqPath, new ChangeIndexPredicate(ChangeField.FILE_PART_SPEC, file));
}
/**
@@ -247,7 +247,7 @@
if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
footer = footer.substring(0, indexEquals) + ": " + footer.substring(indexEquals + 1);
}
- return new ChangeIndexPredicate(ChangeField.FOOTER, footer.toLowerCase(Locale.US));
+ return new ChangeIndexPredicate(ChangeField.FOOTER_SPEC, footer.toLowerCase(Locale.US));
}
/**
@@ -263,7 +263,7 @@
*/
public static Predicate<ChangeData> directory(String directory) {
return new ChangeIndexPredicate(
- ChangeField.DIRECTORY, CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US));
+ ChangeField.DIRECTORY_SPEC, CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US));
}
/** Returns a predicate that matches changes with the provided {@code trackingId}. */
@@ -273,12 +273,13 @@
/** Returns a predicate that matches changes authored by the provided {@code exactAuthor}. */
public static Predicate<ChangeData> exactAuthor(String exactAuthor) {
- return new ChangeIndexPredicate(ChangeField.EXACT_AUTHOR, exactAuthor.toLowerCase(Locale.US));
+ return new ChangeIndexPredicate(
+ ChangeField.EXACT_AUTHOR_SPEC, exactAuthor.toLowerCase(Locale.US));
}
/** Returns a predicate that matches changes authored by the provided {@code author}. */
public static Predicate<ChangeData> author(String author) {
- return new ChangeIndexPredicate(ChangeField.AUTHOR, author);
+ return new ChangeIndexPredicate(ChangeField.AUTHOR_PARTS_SPEC, author);
}
/**
@@ -287,7 +288,7 @@
*/
public static Predicate<ChangeData> exactCommitter(String exactCommitter) {
return new ChangeIndexPredicate(
- ChangeField.EXACT_COMMITTER, exactCommitter.toLowerCase(Locale.US));
+ ChangeField.EXACT_COMMITTER_SPEC, exactCommitter.toLowerCase(Locale.US));
}
/**
@@ -295,7 +296,8 @@
* committer}.
*/
public static Predicate<ChangeData> committer(String comitter) {
- return new ChangeIndexPredicate(ChangeField.COMMITTER, comitter.toLowerCase(Locale.US));
+ return new ChangeIndexPredicate(
+ ChangeField.COMMITTER_PARTS_SPEC, comitter.toLowerCase(Locale.US));
}
/** Returns a predicate that matches changes whose ID starts with the provided {@code id}. */
@@ -308,7 +310,7 @@
* its name.
*/
public static Predicate<ChangeData> projectPrefix(String prefix) {
- return new ChangeIndexPredicate(ChangeField.PROJECTS, prefix);
+ return new ChangeIndexPredicate(ChangeField.PROJECTS_SPEC, prefix);
}
/**
@@ -354,7 +356,7 @@
* to "1", or non-pure reverts if {@code value} is "0".
*/
public static Predicate<ChangeData> pureRevert(String value) {
- return new ChangeIndexPredicate(ChangeField.IS_PURE_REVERT, value);
+ return new ChangeIndexPredicate(ChangeField.IS_PURE_REVERT_SPEC, value);
}
/**
@@ -365,6 +367,6 @@
* com.google.gerrit.entities.SubmitRequirement}s.
*/
public static Predicate<ChangeData> isSubmittable(String value) {
- return new ChangeIndexPredicate(ChangeField.IS_SUBMITTABLE, value);
+ return new ChangeIndexPredicate(ChangeField.IS_SUBMITTABLE_SPEC, value);
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 1c6235b..fe4cb5d 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -29,6 +29,7 @@
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Address;
@@ -43,9 +44,9 @@
import com.google.gerrit.exceptions.NotSignedInException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate;
@@ -476,6 +477,7 @@
}
}
+ @Nullable
Schema<ChangeData> getSchema() {
return index != null ? index.getSchema() : null;
}
@@ -537,16 +539,16 @@
@Operator
public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_BEFORE);
+ checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
return new BeforePredicate(
- ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
+ ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
}
@Operator
public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_AFTER);
+ checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
return new AfterPredicate(
- ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
+ ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
}
@Operator
@@ -670,7 +672,7 @@
}
if ("uploader".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.UPLOADER, "is:uploader");
+ checkFieldAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
return ChangePredicates.uploader(self());
}
@@ -713,12 +715,12 @@
}
if ("pure-revert".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.IS_PURE_REVERT, "is:pure-revert");
+ checkFieldAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
return ChangePredicates.pureRevert("1");
}
if ("submittable".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.IS_SUBMITTABLE)) {
+ if (!args.index.getSchema().hasField(ChangeField.IS_SUBMITTABLE_SPEC)) {
// SubmittablePredicate will match if *any* of the submit records are OK,
// but we need to check that they're *all* OK, so check that none of the
// submit records match any of the negative cases. To avoid checking yet
@@ -729,7 +731,7 @@
Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
}
- checkFieldAvailable(ChangeField.IS_SUBMITTABLE, "is:submittable");
+ checkFieldAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
return new IsSubmittablePredicate();
}
@@ -1221,7 +1223,7 @@
@Operator
public Predicate<ChangeData> uploader(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- checkFieldAvailable(ChangeField.UPLOADER, "uploader");
+ checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploader");
return uploader(parseAccount(who, (AccountState s) -> true));
}
@@ -1281,7 +1283,7 @@
@Operator
public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
- checkFieldAvailable(ChangeField.UPLOADER, "uploaderin");
+ checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
@@ -1620,7 +1622,7 @@
return Predicate.or(predicates);
}
- protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator)
+ protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator)
throws QueryParseException {
if (!args.index.getSchema().hasField(field)) {
throw new QueryParseException(
diff --git a/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java
index 24b8b7a..d1c487e 100644
--- a/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java
@@ -14,17 +14,17 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.RegexPredicate;
public abstract class ChangeRegexPredicate extends RegexPredicate<ChangeData>
implements Matchable<ChangeData> {
- protected ChangeRegexPredicate(FieldDef<ChangeData, ?> def, String value) {
+ protected ChangeRegexPredicate(SchemaField<ChangeData, ?> def, String value) {
super(def, value);
}
- protected ChangeRegexPredicate(FieldDef<ChangeData, ?> def, String name, String value) {
+ protected ChangeRegexPredicate(SchemaField<ChangeData, ?> def, String name, String value) {
super(def, name, value);
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 66c136f..fa48511 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -105,7 +105,7 @@
@Nullable private final Change.Status status;
private ChangeStatusPredicate(@Nullable Change.Status status) {
- super(ChangeField.STATUS, status != null ? canonicalize(status) : INVALID_STATUS);
+ super(ChangeField.STATUS_SPEC, status != null ? canonicalize(status) : INVALID_STATUS);
this.status = status;
}
@@ -151,6 +151,7 @@
return 50_000;
case ABANDONED:
return 50_000;
+ case NEW:
default:
return 2000;
}
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 6aacfc9..5e8674e 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -119,6 +119,7 @@
return count == null ? matchingVotes >= 1 : matchingVotes == count;
}
+ @Nullable
protected static LabelType type(LabelTypes types, String toFind) {
if (types.byLabel(toFind).isPresent()) {
return types.byLabel(toFind).get();
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
index c16bc83..830df98 100644
--- a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
@@ -29,7 +29,7 @@
}
FileExtensionListPredicate(String value) {
- super(ChangeField.ONLY_EXTENSIONS, clean(value));
+ super(ChangeField.ONLY_EXTENSIONS_SPEC, clean(value));
}
@Override
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
index 39715cf..d15c5dc 100644
--- a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
@@ -26,7 +26,7 @@
}
FileExtensionPredicate(String value) {
- super(ChangeField.EXTENSION, clean(value));
+ super(ChangeField.EXTENSION_SPEC, clean(value));
}
@Override
diff --git a/java/com/google/gerrit/server/query/change/HasSubmoduleUpdatePredicate.java b/java/com/google/gerrit/server/query/change/HasSubmoduleUpdatePredicate.java
new file mode 100644
index 0000000..4ff40a4
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/HasSubmoduleUpdatePredicate.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2022 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.change;
+
+import static com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder.SUBMODULE_UPDATE_HAS_ARG;
+
+import com.google.gerrit.entities.Patch.FileMode;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffOperations;
+import com.google.gerrit.server.patch.DiffOptions;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Submit requirement predicate that returns true if the diff of the latest patchset against the
+ * parent number identified by {@link #base} has a submodule modified file, that is, a .gitmodules
+ * or a git link file.
+ */
+public class HasSubmoduleUpdatePredicate extends SubmitRequirementPredicate {
+ private static final String GIT_MODULES_FILE = ".gitmodules";
+
+ private final DiffOperations diffOperations;
+ private final GitRepositoryManager repoManager;
+ private final int base;
+
+ public interface Factory {
+ HasSubmoduleUpdatePredicate create(int base);
+ }
+
+ @Inject
+ HasSubmoduleUpdatePredicate(
+ DiffOperations diffOperations, GitRepositoryManager repoManager, @Assisted int base) {
+ super("has", SUBMODULE_UPDATE_HAS_ARG);
+ this.diffOperations = diffOperations;
+ this.repoManager = repoManager;
+ this.base = base;
+ }
+
+ @Override
+ public boolean match(ChangeData cd) {
+ try {
+ try (Repository repo = repoManager.openRepository(cd.project());
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit revCommit = rw.parseCommit(cd.currentPatchSet().commitId());
+ if (base > revCommit.getParentCount()) {
+ return false;
+ }
+ }
+ Map<String, FileDiffOutput> diffList =
+ diffOperations.listModifiedFilesAgainstParent(
+ cd.project(), cd.currentPatchSet().commitId(), base, DiffOptions.DEFAULTS);
+ return diffList.values().stream().anyMatch(HasSubmoduleUpdatePredicate::isGitLink);
+ } catch (DiffNotAvailableException e) {
+ throw new StorageException(
+ String.format(
+ "Failed to evaluate the diff for commit %s against parent number %d",
+ cd.currentPatchSet().commitId(), base),
+ e);
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format("Failed to open repo for project %s", cd.project()), e);
+ }
+ }
+
+ /**
+ * Return true if the modified file is a {@link #GIT_MODULES_FILE} or a git link regardless of if
+ * the modification type is add, remove or modify.
+ */
+ private static boolean isGitLink(FileDiffOutput fileDiffOutput) {
+ Optional<String> oldPath = fileDiffOutput.oldPath();
+ Optional<String> newPath = fileDiffOutput.newPath();
+ Optional<FileMode> oldMode = fileDiffOutput.oldMode();
+ Optional<FileMode> newMode = fileDiffOutput.newMode();
+
+ return (oldPath.isPresent() && oldPath.get().equals(GIT_MODULES_FILE))
+ || (newPath.isPresent() && newPath.get().equals(GIT_MODULES_FILE))
+ || (oldMode.isPresent() && oldMode.get().equals(FileMode.GITLINK))
+ || (newMode.isPresent() && newMode.get().equals(FileMode.GITLINK));
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java b/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java
index 312c04e..b6059f7 100644
--- a/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.IntegerRangePredicate;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.QueryParseException;
@@ -22,7 +22,7 @@
public abstract class IntegerRangeChangePredicate extends IntegerRangePredicate<ChangeData>
implements Matchable<ChangeData> {
- protected IntegerRangeChangePredicate(FieldDef<ChangeData, Integer> type, String value)
+ protected IntegerRangeChangePredicate(SchemaField<ChangeData, Integer> type, String value)
throws QueryParseException {
super(type, value);
}
diff --git a/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java b/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java
index 17de132..aeee744 100644
--- a/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java
@@ -20,7 +20,7 @@
public class IsSubmittablePredicate extends BooleanPredicate {
public IsSubmittablePredicate() {
- super(ChangeField.IS_SUBMITTABLE);
+ super(ChangeField.IS_SUBMITTABLE_SPEC);
}
/**
@@ -53,11 +53,11 @@
public static Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
if (in instanceof IsSubmittablePredicate) {
return Predicate.and(
- new BooleanPredicate(ChangeField.IS_SUBMITTABLE), ChangeStatusPredicate.open());
+ new BooleanPredicate(ChangeField.IS_SUBMITTABLE_SPEC), ChangeStatusPredicate.open());
}
if (in instanceof NotPredicate && in.getChild(0) instanceof IsSubmittablePredicate) {
return Predicate.or(
- Predicate.not(new BooleanPredicate(ChangeField.IS_SUBMITTABLE)),
+ Predicate.not(new BooleanPredicate(ChangeField.IS_SUBMITTABLE_SPEC)),
ChangeStatusPredicate.closed());
}
return in;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 5a81ca1..e890066 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -99,6 +99,7 @@
return new EqualsLabelPredicate(args, label, value, account, count);
}
+ @Nullable
protected static LabelType type(LabelTypes types, String toFind) {
if (types.byLabel(toFind).isPresent()) {
return types.byLabel(toFind).get();
diff --git a/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
index 1787c76..315785c 100644
--- a/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
@@ -22,7 +22,7 @@
protected final RunAutomaton pattern;
public RegexDirectoryPredicate(String re) {
- super(ChangeField.DIRECTORY, re);
+ super(ChangeField.DIRECTORY_SPEC, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/java/com/google/gerrit/server/query/change/RegexHashtagPredicate.java b/java/com/google/gerrit/server/query/change/RegexHashtagPredicate.java
index 24efa6a..f62780a 100644
--- a/java/com/google/gerrit/server/query/change/RegexHashtagPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexHashtagPredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.query.change;
-import static com.google.gerrit.server.index.change.ChangeField.HASHTAG;
+import static com.google.gerrit.server.index.change.ChangeField.HASHTAG_SPEC;
import dk.brics.automaton.RegExp;
import dk.brics.automaton.RunAutomaton;
@@ -23,7 +23,7 @@
protected final RunAutomaton pattern;
public RegexHashtagPredicate(String re) {
- super(HASHTAG, re);
+ super(HASHTAG_SPEC, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index 4c3c04c..9368047 100644
--- a/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -19,7 +19,7 @@
public class RegexPathPredicate extends ChangeRegexPredicate {
public RegexPathPredicate(String re) {
- super(ChangeField.PATH, re);
+ super(ChangeField.PATH_SPEC, re);
}
@Override
diff --git a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index bbdfc66..a51dcc4 100644
--- a/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -24,7 +24,7 @@
protected final RunAutomaton pattern;
public RegexProjectPredicate(String re) {
- super(ChangeField.PROJECT, re);
+ super(ChangeField.PROJECT_SPEC, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index b2dba72..cc556ba 100644
--- a/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -24,7 +24,7 @@
protected final RunAutomaton pattern;
public RegexRefPredicate(String re) throws QueryParseException {
- super(ChangeField.REF, re);
+ super(ChangeField.REF_SPEC, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 57f5213..b355afb 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -40,7 +40,7 @@
protected final Account.Id id;
private ReviewerPredicate(ReviewerStateInternal state, Account.Id id) {
- super(ChangeField.REVIEWER, ChangeField.getReviewerFieldValue(state, id));
+ super(ChangeField.REVIEWER_SPEC, ChangeField.getReviewerFieldValue(state, id));
this.state = state;
this.id = id;
}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index 2580a1b..3f4c158 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -14,13 +14,16 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.common.base.Splitter;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.query.FileEditsPredicate;
import com.google.gerrit.server.query.FileEditsPredicate.FileEditsArgs;
import com.google.inject.Inject;
+import java.util.List;
+import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@@ -37,6 +40,7 @@
new QueryBuilder.Definition<>(SubmitRequirementChangeQueryBuilder.class);
private final DistinctVotersPredicate.Factory distinctVotersPredicateFactory;
+ private final HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory;
/**
* Regular expression for the {@link #file(String)} operator. Field value is of the form:
@@ -48,20 +52,25 @@
private static final Pattern FILE_EDITS_PATTERN =
Pattern.compile("'((?:(?:\\\\')|(?:[^']))*)',withDiffContaining='((?:(?:\\\\')|(?:[^']))*)'");
+ public static final String SUBMODULE_UPDATE_HAS_ARG = "submodule-update";
+ private static final Splitter SUBMODULE_UPDATE_SPLITTER = Splitter.on(",");
+
private final FileEditsPredicate.Factory fileEditsPredicateFactory;
@Inject
SubmitRequirementChangeQueryBuilder(
Arguments args,
DistinctVotersPredicate.Factory distinctVotersPredicateFactory,
- FileEditsPredicate.Factory fileEditsPredicateFactory) {
+ FileEditsPredicate.Factory fileEditsPredicateFactory,
+ HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory) {
super(def, args);
this.distinctVotersPredicateFactory = distinctVotersPredicateFactory;
this.fileEditsPredicateFactory = fileEditsPredicateFactory;
+ this.hasSubmoduleUpdateFactory = hasSubmoduleUpdateFactory;
}
@Override
- protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator) {
+ protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator) {
// Submit requirements don't rely on the index, so they can be used regardless of index schema
// version.
}
@@ -79,6 +88,37 @@
return super.is(value);
}
+ @Override
+ public Predicate<ChangeData> has(String value) throws QueryParseException {
+ if (value.toLowerCase(Locale.US).startsWith(SUBMODULE_UPDATE_HAS_ARG)) {
+ List<String> args = SUBMODULE_UPDATE_SPLITTER.splitToList(value);
+ if (args.size() > 2) {
+ throw error(
+ String.format(
+ "wrong number of arguments for the has:%s operator", SUBMODULE_UPDATE_HAS_ARG));
+ } else if (args.size() == 2) {
+ List<String> baseValue = Splitter.on("=").splitToList(args.get(1));
+ if (baseValue.size() != 2) {
+ throw error("unexpected base value format");
+ }
+ if (!baseValue.get(0).toLowerCase(Locale.US).equals("base")) {
+ throw error("unexpected base value format");
+ }
+ try {
+ int base = Integer.parseInt(baseValue.get(1));
+ return hasSubmoduleUpdateFactory.create(base);
+ } catch (NumberFormatException e) {
+ throw error(
+ String.format(
+ "failed to parse the parent number %s: %s", baseValue.get(1), e.getMessage()));
+ }
+ } else {
+ return hasSubmoduleUpdateFactory.create(0);
+ }
+ }
+ return super.has(value);
+ }
+
@Operator
public Predicate<ChangeData> authoremail(String who) throws QueryParseException {
return new RegexAuthorEmailPredicate(who);
diff --git a/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java b/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java
index abbd0c9..0b2d32d 100644
--- a/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.TimestampRangePredicate;
import java.sql.Timestamp;
@@ -22,7 +22,7 @@
public abstract class TimestampRangeChangePredicate extends TimestampRangePredicate<ChangeData>
implements Matchable<ChangeData> {
protected TimestampRangeChangePredicate(
- FieldDef<ChangeData, Timestamp> def, String name, String value) {
+ SchemaField<ChangeData, Timestamp> def, String name, String value) {
super(def, name, value);
}
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index a3c48b9..d7a5da11 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -92,7 +93,8 @@
return Response.ok(result);
}
+ @Nullable
private static Boolean toBoolean(boolean v) {
- return v ? v : null;
+ return v ? Boolean.TRUE : null;
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 8d65aac..d8ad3cf 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -19,6 +19,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
@@ -93,6 +94,7 @@
return pwi;
}
+ @Nullable
private static Boolean toBoolean(boolean value) {
return value ? true : null;
}
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 30534b5..79737f3 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -240,7 +240,7 @@
if (suggest) {
return Response.ok(ImmutableList.of());
}
- throw new BadRequestException(e.getMessage());
+ throw new BadRequestException(e.getMessage(), e);
}
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 12abf3d..173f24b 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -131,10 +131,7 @@
try {
starredChangesUtil.star(
- self.get().getAccountId(),
- change.getProject(),
- change.getId(),
- StarredChangesUtil.Operation.ADD);
+ self.get().getAccountId(), change.getId(), StarredChangesUtil.Operation.ADD);
} catch (MutuallyExclusiveLabelsException e) {
throw new ResourceConflictException(e.getMessage());
} catch (IllegalLabelException e) {
@@ -182,10 +179,7 @@
throw new AuthException("not allowed remove starred change");
}
starredChangesUtil.star(
- self.get().getAccountId(),
- rsrc.getChange().getProject(),
- rsrc.getChange().getId(),
- StarredChangesUtil.Operation.REMOVE);
+ self.get().getAccountId(), rsrc.getChange().getId(), StarredChangesUtil.Operation.REMOVE);
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
new file mode 100644
index 0000000..044fa0d
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -0,0 +1,194 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.api.changes.ApplyPatchPatchSetInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+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;
+
+@Singleton
+public class ApplyPatch implements RestModifyView<ChangeResource, ApplyPatchPatchSetInput> {
+ private final ChangeJson.Factory jsonFactory;
+ private final ContributorAgreementsChecker contributorAgreements;
+ private final Provider<IdentifiedUser> user;
+ private final GitRepositoryManager gitManager;
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
+ private final Provider<InternalChangeQuery> queryProvider;
+ private final ZoneId serverZoneId;
+
+ @Inject
+ ApplyPatch(
+ ChangeJson.Factory jsonFactory,
+ ContributorAgreementsChecker contributorAgreements,
+ Provider<IdentifiedUser> user,
+ GitRepositoryManager gitManager,
+ BatchUpdate.Factory batchUpdateFactory,
+ PatchSetInserter.Factory patchSetInserterFactory,
+ Provider<InternalChangeQuery> queryProvider,
+ @GerritPersonIdent PersonIdent myIdent) {
+ this.jsonFactory = jsonFactory;
+ this.contributorAgreements = contributorAgreements;
+ this.user = user;
+ this.gitManager = gitManager;
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.patchSetInserterFactory = patchSetInserterFactory;
+ this.queryProvider = queryProvider;
+ this.serverZoneId = myIdent.getZoneId();
+ }
+
+ @Override
+ public Response<ChangeInfo> apply(ChangeResource rsrc, ApplyPatchPatchSetInput input)
+ throws IOException, UpdateException, RestApiException, PermissionBackendException,
+ ConfigInvalidException, NoSuchProjectException, InvalidChangeOperationException {
+ NameKey project = rsrc.getProject();
+ contributorAgreements.check(project, rsrc.getUser());
+ BranchNameKey destBranch = rsrc.getChange().getDest();
+
+ try (Repository repo = gitManager.openRepository(project);
+ // This inserter and revwalk *must* be passed to any BatchUpdates
+ // created later on, to ensure the applied commit is flushed
+ // before patch sets are updated.
+ ObjectInserter oi = repo.newObjectInserter();
+ ObjectReader reader = oi.newReader();
+ CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
+ Ref destRef = repo.getRefDatabase().exactRef(destBranch.branch());
+ if (destRef == null) {
+ throw new ResourceNotFoundException(
+ String.format("Branch %s does not exist.", destBranch.branch()));
+ }
+ ChangeData destChange = rsrc.getChangeData();
+ if (destChange == null) {
+ throw new PreconditionFailedException(
+ "patch:apply cannot be called without a destination change.");
+ }
+
+ if (destChange.change().isClosed()) {
+ throw new PreconditionFailedException(
+ String.format(
+ "patch:apply with Change-Id %s could not update the existing change %d "
+ + "in destination branch %s of project %s, because the change was closed (%s)",
+ destChange.getId(),
+ destChange.getId().get(),
+ destBranch.branch(),
+ destBranch.project(),
+ destChange.change().getStatus().name()));
+ }
+
+ RevCommit baseCommit =
+ CommitUtil.getBaseCommit(
+ project.get(), queryProvider.get(), revWalk, destRef, input.base);
+ ObjectId treeId = ApplyPatchUtil.applyPatch(repo, oi, input.patch, baseCommit);
+
+ Instant now = TimeUtil.now();
+ PersonIdent committerIdent = user.get().newCommitterIdent(now, serverZoneId);
+ PersonIdent authorIdent =
+ input.author == null
+ ? committerIdent
+ : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
+ String commitMessage =
+ CommitMessageUtil.checkAndSanitizeCommitMessage(
+ input.commitMessage != null
+ ? input.commitMessage
+ : "The following patch was applied:\n>\t"
+ + input.patch.patch.replaceAll("\n", "\n>\t"));
+
+ ObjectId appliedCommit =
+ CommitUtil.createCommitWithTree(
+ oi, authorIdent, committerIdent, baseCommit, commitMessage, treeId);
+ CodeReviewCommit commit = revWalk.parseCommit(appliedCommit);
+ oi.flush();
+
+ Change resultChange;
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
+ bu.setRepository(repo, revWalk, oi);
+ resultChange =
+ insertPatchSet(bu, repo, patchSetInserterFactory, destChange.notes(), commit);
+ } catch (NoSuchChangeException | RepositoryNotFoundException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+ ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
+ ChangeInfo changeInfo = json.format(resultChange);
+ return Response.ok(changeInfo);
+ }
+ }
+
+ private static Change insertPatchSet(
+ BatchUpdate bu,
+ Repository git,
+ PatchSetInserter.Factory patchSetInserterFactory,
+ ChangeNotes destNotes,
+ CodeReviewCommit commit)
+ throws IOException, UpdateException, RestApiException {
+ Change destChange = destNotes.getChange();
+ PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
+ PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, commit);
+ inserter.setMessage(buildMessageForPatchSet(psId));
+ bu.addOp(destChange.getId(), inserter);
+ bu.execute();
+ return inserter.getChange();
+ }
+
+ private static String buildMessageForPatchSet(PatchSet.Id psId) {
+ return new StringBuilder(String.format("Uploaded patch set %s.", psId.get())).toString();
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
new file mode 100644
index 0000000..d4f549a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.eclipse.jgit.api.errors.PatchApplyException;
+import org.eclipse.jgit.api.errors.PatchFormatException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.patch.PatchApplier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+
+/** Utility for applying a patch. */
+public final class ApplyPatchUtil {
+
+ /**
+ * Applies the given patch on top of the merge tip, using the given object inserter.
+ *
+ * @param repo to apply the patch in
+ * @param oi to operate with
+ * @param input the patch for applying
+ * @param mergeTip the tip to apply the patch on
+ * @return the tree ID with the applied patch
+ * @throws IOException if unable to create the jgit PatchApplier object
+ * @throws RestApiException for any other failure
+ */
+ public static ObjectId applyPatch(
+ Repository repo, ObjectInserter oi, ApplyPatchInput input, RevCommit mergeTip)
+ throws IOException, RestApiException {
+ checkNotNull(mergeTip);
+ RevTree tip = mergeTip.getTree();
+ InputStream patchStream =
+ new ByteArrayInputStream(input.patch.getBytes(StandardCharsets.UTF_8));
+ try {
+ PatchApplier applier = new PatchApplier(repo, tip, oi);
+ PatchApplier.Result applyResult = applier.applyPatch(patchStream);
+ return applyResult.getTreeId();
+ } catch (PatchFormatException e) {
+ throw new BadRequestException("Invalid patch format: " + input.patch, e);
+ } catch (PatchApplyException e) {
+ throw RestApiException.wrap("Cannot apply patch: " + input.patch, e);
+ }
+ }
+
+ private ApplyPatchUtil() {}
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 19cfd6a..b688e2d 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -340,8 +340,18 @@
throw new ResourceConflictException("Invalid path: " + path);
}
+ if (fileContentInput.fileMode != null) {
+ if ((fileContentInput.fileMode != 100644) && (fileContentInput.fileMode != 100755)) {
+ throw new BadRequestException(
+ "file_mode ("
+ + fileContentInput.fileMode
+ + ") was invalid: supported values are 0, 644, or 755.");
+ }
+ }
+
try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
- editModifier.modifyFile(repository, rsrc.getNotes(), path, newContent);
+ editModifier.modifyFile(
+ repository, rsrc.getNotes(), path, newContent, fileContentInput.fileMode);
} catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index 718759a..87d8cd0 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -122,6 +122,7 @@
post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
put(CHANGE_KIND, "message").to(PutMessage.class);
post(CHANGE_KIND, "check.submit_requirement").to(CheckSubmitRequirement.class);
+ post(CHANGE_KIND, "patch:apply").to(ApplyPatch.class);
get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 66f8be7..7e7892c 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -31,9 +31,7 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -46,6 +44,7 @@
import com.google.gerrit.server.change.SetCherryPickOp;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.MergeUtil;
@@ -76,8 +75,6 @@
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -85,7 +82,6 @@
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.ChangeIdUtil;
@Singleton
@@ -267,7 +263,9 @@
String.format("Branch %s does not exist.", dest.branch()));
}
- RevCommit baseCommit = getBaseCommit(destRef, project.get(), revWalk, input.base);
+ RevCommit baseCommit =
+ CommitUtil.getBaseCommit(
+ project.get(), queryProvider.get(), revWalk, destRef, input.base);
CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
@@ -384,57 +382,6 @@
}
}
- private RevCommit getBaseCommit(Ref destRef, String project, RevWalk revWalk, String base)
- throws RestApiException, IOException {
- RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId());
- // The tip commit of the destination ref is the default base for the newly created change.
- if (Strings.isNullOrEmpty(base)) {
- return destRefTip;
- }
-
- ObjectId baseObjectId;
- try {
- baseObjectId = ObjectId.fromString(base);
- } catch (InvalidObjectIdException e) {
- throw new BadRequestException(
- String.format("Base %s doesn't represent a valid SHA-1", base), e);
- }
-
- RevCommit baseCommit;
- try {
- baseCommit = revWalk.parseCommit(baseObjectId);
- } catch (MissingObjectException e) {
- throw new UnprocessableEntityException(
- String.format("Base %s doesn't exist", baseObjectId.name()), e);
- }
-
- InternalChangeQuery changeQuery = queryProvider.get();
- changeQuery.enforceVisibility(true);
- List<ChangeData> changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), base);
-
- if (changeDatas.isEmpty()) {
- if (revWalk.isMergedInto(baseCommit, destRefTip)) {
- // The base commit is a merged commit with no change associated.
- return baseCommit;
- }
- throw new UnprocessableEntityException(
- String.format("Commit %s does not exist on branch %s", base, destRef.getName()));
- } else if (changeDatas.size() != 1) {
- throw new ResourceConflictException("Multiple changes found for commit " + base);
- }
-
- Change change = changeDatas.get(0).change();
- if (!change.isAbandoned()) {
- // The base commit is a valid change revision.
- return baseCommit;
- }
-
- throw new ResourceConflictException(
- String.format(
- "Change %s with commit %s is %s",
- change.getChangeId(), base, ChangeUtil.status(change)));
- }
-
private Change.Id insertPatchSet(
BatchUpdate bu,
Repository git,
@@ -529,7 +476,7 @@
reviewers.remove(user.get().getAccountId());
Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
ccs.remove(user.get().getAccountId());
- ins.setReviewersAndCcs(reviewers, ccs);
+ ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
}
// If there is a base, and the base is not merged, the groups will be overridden by the base's
// groups.
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 81b6fb3..8ebe71f 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -263,6 +263,7 @@
return rci;
}
+ @Nullable
private List<FixSuggestionInfo> toFixSuggestionInfos(
@Nullable List<FixSuggestion> fixSuggestions) {
if (fixSuggestions == null || fixSuggestions.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 760d99d..2cb427a 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -61,6 +61,7 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.MergeUtilFactory;
@@ -94,7 +95,6 @@
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NoMergeBaseException;
-import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -293,6 +293,10 @@
}
}
+ if (input.merge != null && input.patch != null) {
+ throw new BadRequestException("Only one of `merge` and `patch` arguments can be set.");
+ }
+
if (input.author != null
&& (Strings.isNullOrEmpty(input.author.email)
|| Strings.isNullOrEmpty(input.author.name))) {
@@ -373,9 +377,19 @@
"merge commit has conflicts in the following files: %s",
c.getFilesWithGitConflicts());
}
+ } else if (input.patch != null) {
+ // create a commit with the given patch.
+ if (mergeTip == null) {
+ throw new BadRequestException("Cannot apply patch on top of an empty tree.");
+ }
+ ObjectId treeId = ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
+ c =
+ rw.parseCommit(
+ CommitUtil.createCommitWithTree(
+ oi, author, committer, mergeTip, commitMessage, treeId));
} else {
- // create an empty commit
- c = newCommit(oi, rw, author, committer, mergeTip, commitMessage);
+ // create an empty commit.
+ c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
}
// Flush inserter so that commit becomes visible to validators
oi.flush();
@@ -526,7 +540,7 @@
return commitMessage;
}
- private static CodeReviewCommit newCommit(
+ private static CodeReviewCommit createEmptyCommit(
ObjectInserter oi,
CodeReviewRevWalk rw,
PersonIdent authorIdent,
@@ -535,17 +549,14 @@
String commitMessage)
throws IOException {
logger.atFine().log("Creating empty commit");
- CommitBuilder commit = new CommitBuilder();
- if (mergeTip == null) {
- commit.setTreeId(emptyTreeId(oi));
- } else {
- commit.setTreeId(mergeTip.getTree().getId());
- commit.setParentId(mergeTip);
- }
- commit.setAuthor(authorIdent);
- commit.setCommitter(committerIdent);
- commit.setMessage(commitMessage);
- return rw.parseCommit(insert(oi, commit));
+ ObjectId treeID = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree().getId();
+ return rw.parseCommit(
+ CommitUtil.createCommitWithTree(
+ oi, authorIdent, committerIdent, mergeTip, commitMessage, treeID));
+ }
+
+ private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
+ return inserter.insert(new TreeFormatter());
}
private CodeReviewCommit newMergeCommit(
@@ -615,14 +626,4 @@
return stringBuilder.toString();
}
-
- private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) throws IOException {
- ObjectId id = inserter.insert(commit);
- inserter.flush();
- return id;
- }
-
- private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
- return inserter.insert(new TreeFormatter());
- }
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index d818210..66171c4 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -97,6 +98,7 @@
return true;
}
+ @Nullable
public Account.Id getDeletedAssignee() {
return deletedAssignee != null ? deletedAssignee.account().id() : null;
}
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index e996169..7699873 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -370,6 +370,7 @@
: Iterables.getFirst(fileDiffList.values(), null).oldCommitId();
}
+ @Nullable
private ObjectId getNewId(Map<String, FileDiffOutput> fileDiffList) {
return fileDiffList.isEmpty()
? null
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index a81171a..d126d8a 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -144,6 +144,7 @@
cds, this, Streams.stream(pdiFactories.entries()));
}
+ @Nullable
private ObjectId verifyMetaId(Change change, @Nullable ObjectId id) throws RestApiException {
if (id == null) {
return null;
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 4e5027b..947ca6a 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -42,7 +42,6 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
@@ -53,11 +52,8 @@
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.WalkSorter;
import com.google.gerrit.server.change.WalkSorter.PatchSetData;
-import com.google.gerrit.server.extensions.events.ChangeReverted;
import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.send.MessageIdGenerator;
-import com.google.gerrit.server.mail.send.RevertedSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.permissions.ChangePermission;
@@ -73,7 +69,6 @@
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.PostUpdateContext;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -115,17 +110,13 @@
private final ChangeJson.Factory json;
private final GitRepositoryManager repoManager;
private final WalkSorter sorter;
- private final ChangeMessagesUtil cmUtil;
private final CommitUtil commitUtil;
private final ChangeNotes.Factory changeNotesFactory;
- private final ChangeReverted changeReverted;
- private final RevertedSender.Factory revertedSenderFactory;
private final Sequences seq;
private final NotifyResolver notifyResolver;
private final BatchUpdate.Factory updateFactory;
private final ChangeResource.Factory changeResourceFactory;
private final GetRelated getRelated;
- private final MessageIdGenerator messageIdGenerator;
private CherryPickInput cherryPickInput;
private List<ChangeInfo> results;
@@ -145,17 +136,13 @@
ChangeJson.Factory json,
GitRepositoryManager repoManager,
WalkSorter sorter,
- ChangeMessagesUtil cmUtil,
CommitUtil commitUtil,
ChangeNotes.Factory changeNotesFactory,
- ChangeReverted changeReverted,
- RevertedSender.Factory revertedSenderFactory,
Sequences seq,
NotifyResolver notifyResolver,
BatchUpdate.Factory updateFactory,
ChangeResource.Factory changeResourceFactory,
- GetRelated getRelated,
- MessageIdGenerator messageIdGenerator) {
+ GetRelated getRelated) {
this.queryProvider = queryProvider;
this.user = user;
this.permissionBackend = permissionBackend;
@@ -166,17 +153,13 @@
this.json = json;
this.repoManager = repoManager;
this.sorter = sorter;
- this.cmUtil = cmUtil;
this.commitUtil = commitUtil;
this.changeNotesFactory = changeNotesFactory;
- this.changeReverted = changeReverted;
- this.revertedSenderFactory = revertedSenderFactory;
this.seq = seq;
this.notifyResolver = notifyResolver;
this.updateFactory = updateFactory;
this.changeResourceFactory = changeResourceFactory;
this.getRelated = getRelated;
- this.messageIdGenerator = messageIdGenerator;
results = new ArrayList<>();
cherryPickInput = null;
}
@@ -257,9 +240,6 @@
cherryPickInput.base = null;
Project.NameKey project = projectAndBranch.project();
cherryPickInput.destination = projectAndBranch.branch();
- if (revertInput.workInProgress) {
- cherryPickInput.notify = firstNonNull(cherryPickInput.notify, NotifyHandling.OWNER);
- }
Collection<ChangeData> changesInProjectAndBranch =
changesPerProjectAndBranch.get(projectAndBranch);
@@ -336,11 +316,10 @@
cherryPickRevertChangeId,
timestamp,
revertInput.workInProgress));
- bu.addOp(changeNotes.getChange().getId(), new PostRevertedMessageOp(generatedChangeId));
- bu.addOp(
- cherryPickRevertChangeId,
- new NotifyOp(changeNotes.getChange(), cherryPickRevertChangeId));
-
+ if (!revertInput.workInProgress) {
+ commitUtil.addChangeRevertedNotificationOps(
+ bu, changeNotes.getChangeId(), cherryPickRevertChangeId, generatedChangeId.name());
+ }
bu.execute();
}
}
@@ -366,6 +345,9 @@
// change is created for the cherry-picked commit. Notifications are sent only for this change,
// but not for the intermediately created revert commit.
cherryPickInput.notify = revertInput.notify;
+ if (revertInput.workInProgress) {
+ cherryPickInput.notify = firstNonNull(cherryPickInput.notify, NotifyHandling.NONE);
+ }
cherryPickInput.notifyDetails = revertInput.notifyDetails;
cherryPickInput.parent = 1;
cherryPickInput.keepReviewers = true;
@@ -598,55 +580,4 @@
return true;
}
}
-
- private class NotifyOp implements BatchUpdateOp {
- private final Change change;
- private final Change.Id revertChangeId;
-
- NotifyOp(Change change, Change.Id revertChangeId) {
- this.change = change;
- this.revertChangeId = revertChangeId;
- }
-
- @Override
- public void postUpdate(PostUpdateContext ctx) throws Exception {
- changeReverted.fire(
- ctx.getChangeData(change),
- ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertChangeId)),
- ctx.getWhen());
- try {
- RevertedSender emailSender = revertedSenderFactory.create(ctx.getProject(), change.getId());
- emailSender.setFrom(ctx.getAccountId());
- emailSender.setNotify(ctx.getNotify(change.getId()));
- emailSender.setMessageId(
- messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
- emailSender.send();
- } catch (Exception err) {
- logger.atSevere().withCause(err).log(
- "Cannot send email for revert change %s", change.getId());
- }
- }
- }
-
- /**
- * create a message that describes the revert if the cherry-pick is successful, and point the
- * revert of the change towards the cherry-pick. The cherry-pick is the updated change that acts
- * as "revert-of" the original change.
- */
- private class PostRevertedMessageOp implements BatchUpdateOp {
- private final ObjectId computedChangeId;
-
- PostRevertedMessageOp(ObjectId computedChangeId) {
- this.computedChangeId = computedChangeId;
- }
-
- @Override
- public boolean updateChange(ChangeContext ctx) throws Exception {
- cmUtil.setChangeMessage(
- ctx,
- "Created a revert of this change as I" + computedChangeId.getName(),
- ChangeMessagesUtil.TAG_REVERT);
- return true;
- }
- }
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index d8d51d4..07e54ce 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -208,7 +208,7 @@
queryProvider
.get()
.setLimit(numberOfRelevantChanges)
- .setRequestedFields(ChangeField.REVIEWER)
+ .setRequestedFields(ChangeField.REVIEWER_SPEC)
.query(changeQueryBuilder.owner("self"));
Map<Account.Id, MutableDouble> suggestions = new LinkedHashMap<>();
// Put those candidates at the bottom of the list
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 9f019b6..a587ecc 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.change.WorkInProgressOp.Input;
+import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
@@ -42,11 +43,16 @@
implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
private final BatchUpdate.Factory updateFactory;
private final WorkInProgressOp.Factory opFactory;
+ private final CommitUtil commitUtil;
@Inject
- SetReadyForReview(BatchUpdate.Factory updateFactory, WorkInProgressOp.Factory opFactory) {
+ SetReadyForReview(
+ BatchUpdate.Factory updateFactory,
+ WorkInProgressOp.Factory opFactory,
+ CommitUtil commitUtil) {
this.updateFactory = updateFactory;
this.opFactory = opFactory;
+ this.commitUtil = commitUtil;
}
@Override
@@ -66,6 +72,10 @@
try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
+ if (change.getRevertOf() != null) {
+ commitUtil.addChangeRevertedNotificationOps(
+ bu, change.getRevertOf(), change.getId(), change.getKey().get());
+ }
bu.execute();
return Response.ok();
}
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 560f4e0..5fc4f41 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -234,6 +234,7 @@
* @param user the user who is checking to submit
* @return a reason why any of the changes is not submittable or null
*/
+ @Nullable
private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) {
try {
if (cs.furtherHiddenChanges()) {
@@ -297,6 +298,7 @@
return null;
}
+ @Nullable
@Override
public UiAction.Description getDescription(RevisionResource resource)
throws IOException, PermissionBackendException {
@@ -372,6 +374,7 @@
.setEnabled(Boolean.TRUE.equals(enabled));
}
+ @Nullable
public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws IOException {
Set<ChangeData> mergeabilityMap = new HashSet<>();
Set<ObjectId> outDatedPatchsets = new HashSet<>();
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 09052a6..103a5ac 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -19,6 +19,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.ContributorAgreement;
import com.google.gerrit.extensions.common.AccountDefaultDisplayName;
import com.google.gerrit.extensions.common.AccountsInfo;
@@ -220,7 +221,7 @@
ChangeConfigInfo info = new ChangeConfigInfo();
info.allowBlame = toBoolean(config.getBoolean("change", "allowBlame", true));
boolean hasAssigneeInIndex =
- indexes.getSearchIndex().getSchema().hasField(ChangeField.ASSIGNEE);
+ indexes.getSearchIndex().getSchema().hasField(ChangeField.ASSIGNEE_SPEC);
info.showAssigneeInChangesTable =
toBoolean(
config.getBoolean("change", "showAssigneeInChangesTable", false) && hasAssigneeInIndex);
@@ -303,6 +304,7 @@
return info;
}
+ @Nullable
private String getDocUrl() {
String docUrl = config.getString("gerrit", null, "docUrl");
if (Strings.isNullOrEmpty(docUrl)) {
@@ -328,6 +330,7 @@
private static final String DEFAULT_THEME = "/static/" + SitePaths.THEME_FILENAME;
private static final String DEFAULT_THEME_JS = "/static/" + SitePaths.THEME_JS_FILENAME;
+ @Nullable
private String getDefaultTheme() {
if (config.getString("theme", null, "enableDefault") == null) {
// If not explicitly enabled or disabled, check for the existence of the theme file.
@@ -344,6 +347,7 @@
return null;
}
+ @Nullable
private SshdInfo getSshdInfo() {
String[] addr = config.getStringList("sshd", null, "listenAddress");
if (addr.length == 1 && isOff(addr[0])) {
@@ -380,7 +384,8 @@
return Arrays.asList(config.getStringList("dashboard", null, "submitRequirementColumns"));
}
+ @Nullable
private static Boolean toBoolean(boolean v) {
- return v ? v : null;
+ return v ? Boolean.TRUE : null;
}
}
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index d0a1498..34cf550 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.config;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.restapi.Response;
@@ -90,14 +91,22 @@
private TaskSummaryInfo getTaskSummary() {
Collection<Task<?>> pending = workQueue.getTasks();
int tasksTotal = pending.size();
+ int tasksStopping = 0;
int tasksRunning = 0;
+ int tasksStarting = 0;
int tasksReady = 0;
int tasksSleeping = 0;
for (Task<?> task : pending) {
switch (task.getState()) {
+ case STOPPING:
+ tasksStopping++;
+ break;
case RUNNING:
tasksRunning++;
break;
+ case STARTING:
+ tasksStarting++;
+ break;
case READY:
tasksReady++;
break;
@@ -113,7 +122,9 @@
TaskSummaryInfo taskSummary = new TaskSummaryInfo();
taskSummary.total = toInteger(tasksTotal);
+ taskSummary.stopping = toInteger(tasksStopping);
taskSummary.running = toInteger(tasksRunning);
+ taskSummary.starting = toInteger(tasksStarting);
taskSummary.ready = toInteger(tasksReady);
taskSummary.sleeping = toInteger(tasksSleeping);
return taskSummary;
@@ -211,6 +222,7 @@
return jvmSummary;
}
+ @Nullable
private static Integer toInteger(int i) {
return i != 0 ? i : null;
}
@@ -247,7 +259,9 @@
public static class TaskSummaryInfo {
public Integer total;
+ public Integer stopping;
public Integer running;
+ public Integer starting;
public Integer ready;
public Integer sleeping;
}
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index e617931..9d36aaa 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -17,6 +17,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -181,6 +182,7 @@
return Response.created(json.format(new InternalGroupDescription(createGroup(args))));
}
+ @Nullable
private AccountGroup.UUID owner(GroupInput input) throws UnprocessableEntityException {
if (input.ownerId != null) {
GroupDescription.Internal d = groups.parseInternal(Url.decode(input.ownerId));
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index b94e44d..4d9a1e9 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -21,6 +21,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
@@ -408,6 +409,7 @@
}
}
+ @Nullable
private Pattern getRegexPattern() {
return Strings.isNullOrEmpty(matchRegex) ? null : Pattern.compile(matchRegex);
}
diff --git a/java/com/google/gerrit/server/restapi/project/BanCommit.java b/java/com/google/gerrit/server/restapi/project/BanCommit.java
index eb5473d..2dd7bd8 100644
--- a/java/com/google/gerrit/server/restapi/project/BanCommit.java
+++ b/java/com/google/gerrit/server/restapi/project/BanCommit.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.projects.BanCommitInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -64,6 +65,7 @@
return Response.ok(r);
}
+ @Nullable
private static List<String> transformCommits(List<ObjectId> commits) {
if (commits == null || commits.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java
index 904a16f..192e624 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java
@@ -17,6 +17,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
@@ -125,6 +126,7 @@
return info;
}
+ @Nullable
private static Map<String, Map<String, ConfigParameterInfo>> getPluginConfig(
ProjectState project,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
diff --git a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
index ca48109..455358a 100644
--- a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
@@ -225,6 +225,7 @@
return info;
}
+ @Nullable
private static String replace(String project, String input) {
return input == null ? input : input.replace("${project}", project);
}
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 651e7f0..e1a3c0c 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -26,6 +26,7 @@
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
@@ -340,6 +341,7 @@
return accessSectionInfo;
}
+ @Nullable
private static Boolean toBoolean(boolean value) {
return value ? true : null;
}
diff --git a/java/com/google/gerrit/server/rules/RulesCache.java b/java/com/google/gerrit/server/rules/RulesCache.java
index 773c75e..710c734 100644
--- a/java/com/google/gerrit/server/rules/RulesCache.java
+++ b/java/com/google/gerrit/server/rules/RulesCache.java
@@ -191,6 +191,7 @@
return pmc;
}
+ @Nullable
private PrologMachineCopy consultRules(String name, Reader rules) throws CompileException {
BufferingPrologControl ctl = newEmptyMachine(systemLoader);
PushbackReader in = new PushbackReader(rules, Prolog.PUSHBACK_SIZE);
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 9907b1c..09f142b 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -149,19 +149,16 @@
config.upsertAccessSection(
AccessSection.HEADS,
- heads -> {
- initDefaultAclsForRegisteredUsers(heads, codeReviewLabel, config);
- });
+ heads -> initDefaultAclsForRegisteredUsers(heads, codeReviewLabel, config));
config.upsertAccessSection(
AccessSection.GLOBAL_CAPABILITIES,
- capabilities -> {
- input
- .serviceUsersGroup()
- .ifPresent(
- batchUsersGroup ->
- initDefaultAclsForBatchUsers(capabilities, config, batchUsersGroup));
- });
+ capabilities ->
+ input
+ .serviceUsersGroup()
+ .ifPresent(
+ batchUsersGroup ->
+ initDefaultAclsForBatchUsers(capabilities, config, batchUsersGroup)));
input
.administratorsGroup()
@@ -171,16 +168,10 @@
private void initDefaultAclsForRegisteredUsers(
AccessSection.Builder heads, LabelType codeReviewLabel, ProjectConfig config) {
config.upsertAccessSection(
- "refs/for/*",
- refsFor -> {
- grant(config, refsFor, Permission.ADD_PATCH_SET, registered);
- });
+ "refs/for/*", refsFor -> grant(config, refsFor, Permission.ADD_PATCH_SET, registered));
config.upsertAccessSection(
- "refs/meta/version",
- version -> {
- grant(config, version, Permission.READ, anonymous);
- });
+ "refs/meta/version", version -> grant(config, version, Permission.READ, anonymous));
grant(config, heads, codeReviewLabel, -1, 1, registered);
grant(config, heads, Permission.FORGE_AUTHOR, registered);
@@ -208,15 +199,11 @@
ProjectConfig config, LabelType codeReviewLabel, GroupReference adminsGroup) {
config.upsertAccessSection(
AccessSection.GLOBAL_CAPABILITIES,
- capabilities -> {
- grant(config, capabilities, GlobalCapability.ADMINISTRATE_SERVER, adminsGroup);
- });
+ capabilities ->
+ grant(config, capabilities, GlobalCapability.ADMINISTRATE_SERVER, adminsGroup));
config.upsertAccessSection(
- AccessSection.ALL,
- all -> {
- grant(config, all, Permission.READ, adminsGroup);
- });
+ AccessSection.ALL, all -> grant(config, all, Permission.READ, adminsGroup));
config.upsertAccessSection(
AccessSection.HEADS,
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
new file mode 100644
index 0000000..6be7438
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -0,0 +1,411 @@
+// Copyright (C) 2022 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.schema;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectLevelConfig;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+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.treewalk.TreeWalk;
+
+/**
+ * A class with logic for migrating existing label functions to submit requirements and resetting
+ * the label functions to {@link LabelFunction#NO_BLOCK}.
+ *
+ * <p>Important note: Callers should do this migration only if this gerrit installation has no
+ * Prolog submit rules (i.e. no rules.pl file in refs/meta/config). Otherwise, the newly created
+ * submit requirements might not behave as intended.
+ *
+ * <p>The conversion is done as follows:
+ *
+ * <ul>
+ * <li>MaxWithBlock is translated to submittableIf = label:$lbl=MAX AND -label:$lbl=MIN
+ * <li>MaxNoBlock is translated to submittableIf = label:$lbl=MAX
+ * <li>AnyWithBlock is translated to submittableIf = -label:$lbl=MIN
+ * <li>NoBlock/NoOp are translated to applicableIf = is:false (not applicable)
+ * <li>PatchSetLock labels are left as is
+ * </ul>
+ *
+ * If the label has {@link LabelType#isIgnoreSelfApproval()}, the max vote is appended with the
+ * 'user=non_uploader' argument.
+ *
+ * <p>If there is an existing label and there exists a "submit requirement" with the same name, then
+ * the logic will leave the "submit requirement" as is and will not replace it. But in this case,
+ * the label will be reset to NO_BLOCK anyway.
+ */
+public class MigrateLabelFunctionsToSubmitRequirement {
+ public static final String COMMIT_MSG = "Migrate label functions to submit requirements";
+ private final GitRepositoryManager repoManager;
+ private final PersonIdent serverUser;
+
+ public enum Status {
+ /**
+ * The migrator updated the project config and created new submit requirements and/or did reset
+ * label functions.
+ */
+ MIGRATED,
+
+ /** The project had prolog rules, and the migration was skipped. */
+ HAS_PROLOG,
+
+ /**
+ * The project was migrated with a previous run of this class. The migration for this run was
+ * skipped.
+ */
+ PREVIOUSLY_MIGRATED,
+
+ /**
+ * Migration was run for the project but did not update the project.config because it was
+ * up-to-date.
+ */
+ NO_CHANGE
+ }
+
+ @Inject
+ public MigrateLabelFunctionsToSubmitRequirement(
+ GitRepositoryManager repoManager, @GerritPersonIdent PersonIdent serverUser) {
+ this.repoManager = repoManager;
+ this.serverUser = serverUser;
+ }
+
+ /**
+ * For each label function, create a corresponding submit-requirement and set the label function
+ * to NO_BLOCK. Blocking label functions are substituted with blocking submit-requirements.
+ * Non-blocking label functions are substituted with non-applicable submit requirements, allowing
+ * the label vote to be surfaced as a trigger vote (optional label).
+ *
+ * @return {@link Status} reflecting the status of the migration.
+ */
+ public Status executeMigration(Project.NameKey project, UpdateUI ui)
+ throws IOException, ConfigInvalidException {
+ if (hasPrologRules(project)) {
+ ui.message(String.format("Skipping project %s because it has prolog rules", project));
+ return Status.HAS_PROLOG;
+ }
+ ProjectLevelConfig.Bare projectConfig =
+ new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
+ boolean migrationPerformed = false;
+ try (Repository repo = repoManager.openRepository(project);
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo)) {
+ if (hasMigrationAlreadyRun(repo)) {
+ ui.message(
+ String.format(
+ "Skipping migrating label functions to submit requirements for project '%s'"
+ + " because it has been previously migrated",
+ project));
+ return Status.PREVIOUSLY_MIGRATED;
+ }
+ projectConfig.load(project, repo);
+ Config cfg = projectConfig.getConfig();
+ Map<String, LabelAttributes> labelTypes = getLabelTypes(cfg);
+ Map<String, SubmitRequirement> existingSubmitRequirements = loadSubmitRequirements(cfg);
+ boolean updated = false;
+ for (Map.Entry<String, LabelAttributes> lt : labelTypes.entrySet()) {
+ String labelName = lt.getKey();
+ LabelAttributes attributes = lt.getValue();
+ if (attributes.function().equals("PatchSetLock")) {
+ // PATCH_SET_LOCK functions should be left as is
+ continue;
+ }
+ // If the function is other than "NoBlock" we want to reset the label function regardless
+ // of whether there exists a "submit requirement".
+ if (!attributes.function().equals("NoBlock")) {
+ updated = true;
+ writeLabelFunction(cfg, labelName, "NoBlock");
+ }
+ SubmitRequirement sr = createSrFromLabelDef(labelName, attributes);
+ // Make the operation idempotent by skipping creating the submit-requirement if one was
+ // already created or previously existed.
+ if (existingSubmitRequirements.containsKey(labelName.toLowerCase(Locale.ROOT))) {
+ if (!sr.equals(existingSubmitRequirements.get(labelName.toLowerCase(Locale.ROOT)))) {
+ ui.message(
+ String.format(
+ "Warning: Skipping creating a submit requirement for label '%s'. An existing "
+ + "submit requirement is already present but its definition is not "
+ + "identical to the existing label definition.",
+ labelName));
+ }
+ continue;
+ }
+ updated = true;
+ ui.message(
+ String.format(
+ "Project %s: Creating a submit requirement for label %s", project, labelName));
+ writeSubmitRequirement(cfg, sr);
+ }
+ if (updated) {
+ commit(projectConfig, md);
+ migrationPerformed = true;
+ }
+ }
+ return migrationPerformed ? Status.MIGRATED : Status.NO_CHANGE;
+ }
+
+ /**
+ * Returns a Map containing label names as string in keys along with some of its attributes (that
+ * we need in the migration) like canOverride, ignoreSelfApproval and function in the value.
+ */
+ private Map<String, LabelAttributes> getLabelTypes(Config cfg) {
+ Map<String, LabelAttributes> labelTypes = new HashMap<>();
+ for (String labelName : cfg.getSubsections(ProjectConfig.LABEL)) {
+ String function = cfg.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION);
+ boolean canOverride =
+ cfg.getBoolean(
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_CAN_OVERRIDE,
+ /* defaultValue= */ true);
+ boolean ignoreSelfApproval =
+ cfg.getBoolean(
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_IGNORE_SELF_APPROVAL,
+ /* defaultValue= */ false);
+ LabelAttributes attributes =
+ LabelAttributes.create(
+ function == null ? "MaxWithBlock" : function, canOverride, ignoreSelfApproval);
+ labelTypes.put(labelName, attributes);
+ }
+ return labelTypes;
+ }
+
+ private void writeSubmitRequirement(Config cfg, SubmitRequirement sr) {
+ if (sr.description().isPresent()) {
+ cfg.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ sr.name(),
+ ProjectConfig.KEY_SR_DESCRIPTION,
+ sr.description().get());
+ }
+ if (sr.applicabilityExpression().isPresent()) {
+ cfg.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ sr.name(),
+ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
+ sr.applicabilityExpression().get().expressionString());
+ }
+ cfg.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ sr.name(),
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ sr.submittabilityExpression().expressionString());
+ if (sr.overrideExpression().isPresent()) {
+ cfg.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ sr.name(),
+ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
+ sr.overrideExpression().get().expressionString());
+ }
+ cfg.setBoolean(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ sr.name(),
+ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ sr.allowOverrideInChildProjects());
+ }
+
+ private void writeLabelFunction(Config cfg, String labelName, String function) {
+ cfg.setString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, function);
+ }
+
+ private void commit(ProjectLevelConfig.Bare projectConfig, MetaDataUpdate md) throws IOException {
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ md.setMessage(COMMIT_MSG);
+ projectConfig.commit(md);
+ }
+
+ private static SubmitRequirement createSrFromLabelDef(
+ String labelName, LabelAttributes attributes) {
+ if (!isBlockingOrRequiredLabel(attributes.function())) {
+ return createNonApplicableSr(labelName, attributes.canOverride());
+ }
+ return createBlockingOrRequiredSr(labelName, attributes);
+ }
+
+ private static SubmitRequirement createNonApplicableSr(String labelName, boolean canOverride) {
+ return SubmitRequirement.builder()
+ .setName(labelName)
+ .setApplicabilityExpression(SubmitRequirementExpression.of("is:false"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("is:true"))
+ .setAllowOverrideInChildProjects(canOverride)
+ .build();
+ }
+
+ /**
+ * Create a "submit requirement" that is only satisfied if the label is voted with the max votes
+ * and/or not voted by the min vote, according to the label attributes.
+ */
+ private static SubmitRequirement createBlockingOrRequiredSr(
+ String labelName, LabelAttributes attributes) {
+ SubmitRequirement.Builder builder =
+ SubmitRequirement.builder()
+ .setName(labelName)
+ .setAllowOverrideInChildProjects(attributes.canOverride());
+ String maxPart =
+ String.format("label:%s=MAX", labelName)
+ + (attributes.ignoreSelfApproval() ? ",user=non_uploader" : "");
+ switch (attributes.function()) {
+ case "MaxWithBlock":
+ builder.setSubmittabilityExpression(
+ SubmitRequirementExpression.create(
+ String.format("%s AND -label:%s=MIN", maxPart, labelName)));
+ break;
+ case "AnyWithBlock":
+ builder.setSubmittabilityExpression(
+ SubmitRequirementExpression.create(String.format("-label:%s=MIN", labelName)));
+ break;
+ case "MaxNoBlock":
+ builder.setSubmittabilityExpression(SubmitRequirementExpression.create(maxPart));
+ break;
+ default:
+ break;
+ }
+ return builder.build();
+ }
+
+ private static boolean isBlockingOrRequiredLabel(String function) {
+ return function.equals("AnyWithBlock")
+ || function.equals("MaxWithBlock")
+ || function.equals("MaxNoBlock");
+ }
+
+ public boolean anyProjectHasProlog(Collection<Project.NameKey> allProjects) throws IOException {
+ for (Project.NameKey p : allProjects) {
+ if (hasPrologRules(p)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasPrologRules(Project.NameKey project) throws IOException {
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk rw = new RevWalk(repo);
+ ObjectReader reader = rw.getObjectReader()) {
+ Ref refsConfig = repo.exactRef(RefNames.REFS_CONFIG);
+ if (refsConfig == null) {
+ // Project does not have a refs/meta/config and no rules.pl consequently.
+ return false;
+ }
+ RevCommit commit = repo.parseCommit(refsConfig.getObjectId());
+ try (TreeWalk tw = TreeWalk.forPath(reader, "rules.pl", commit.getTree())) {
+ if (tw != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Returns a map containing submit requirement names in lower name as keys, with {@link
+ * com.google.gerrit.entities.SubmitRequirement} as value.
+ */
+ private Map<String, SubmitRequirement> loadSubmitRequirements(Config rc) {
+ Map<String, SubmitRequirement> allRequirements = new LinkedHashMap<>();
+ for (String name : rc.getSubsections(ProjectConfig.SUBMIT_REQUIREMENT)) {
+ String description =
+ rc.getString(ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_DESCRIPTION);
+ String applicabilityExpr =
+ rc.getString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ name,
+ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION);
+ String submittabilityExpr =
+ rc.getString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ name,
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION);
+ String overrideExpr =
+ rc.getString(
+ ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION);
+ boolean canInherit =
+ rc.getBoolean(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ name,
+ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ false);
+ SubmitRequirement submitRequirement =
+ SubmitRequirement.builder()
+ .setName(name)
+ .setDescription(Optional.ofNullable(description))
+ .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
+ .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
+ .setAllowOverrideInChildProjects(canInherit)
+ .build();
+ allRequirements.put(name.toLowerCase(Locale.ROOT), submitRequirement);
+ }
+ return allRequirements;
+ }
+
+ private static boolean hasMigrationAlreadyRun(Repository repo) throws IOException {
+ try (RevWalk revWalk = new RevWalk(repo)) {
+ Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG);
+ if (refsMetaConfig == null) {
+ return false;
+ }
+ revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId()));
+ RevCommit commit;
+ while ((commit = revWalk.next()) != null) {
+ if (COMMIT_MSG.equals(commit.getShortMessage())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ @AutoValue
+ abstract static class LabelAttributes {
+ abstract String function();
+
+ abstract boolean canOverride();
+
+ abstract boolean ignoreSelfApproval();
+
+ static LabelAttributes create(
+ String function, boolean canOverride, boolean ignoreSelfApproval) {
+ return new AutoValue_MigrateLabelFunctionsToSubmitRequirement_LabelAttributes(
+ function, canOverride, ignoreSelfApproval);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/schema/SchemaModule.java b/java/com/google/gerrit/server/schema/SchemaModule.java
index 9593522..e0e64a3 100644
--- a/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -16,7 +16,7 @@
import static com.google.inject.Scopes.SINGLETON;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -55,7 +55,7 @@
.toProvider(GerritServerIdProvider.class)
.in(SINGLETON);
- bind(new TypeLiteral<ImmutableList<String>>() {})
+ bind(new TypeLiteral<ImmutableSet<String>>() {})
.annotatedWith(GerritImportedServerIds.class)
.toProvider(GerritImportedServerIdsProvider.class)
.in(SINGLETON);
diff --git a/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
index 02ff159..37e7278 100644
--- a/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
+++ b/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.securestore;
import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.ProvisionException;
@@ -54,6 +55,7 @@
return sec.getStringList(section, subsection, name);
}
+ @Nullable
@Override
public synchronized String[] getListForPlugin(
String pluginName, String section, String subsection, String name) {
diff --git a/java/com/google/gerrit/server/securestore/SecureStore.java b/java/com/google/gerrit/server/securestore/SecureStore.java
index b53e38c..855c978 100644
--- a/java/com/google/gerrit/server/securestore/SecureStore.java
+++ b/java/com/google/gerrit/server/securestore/SecureStore.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.securestore;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import java.util.List;
/**
@@ -53,6 +54,7 @@
*
* @return decrypted String value or {@code null} if not found
*/
+ @Nullable
public final String get(String section, String subsection, String name) {
String[] values = getList(section, subsection, name);
if (values != null && values.length > 0) {
@@ -67,6 +69,7 @@
*
* @return decrypted String value or {@code null} if not found
*/
+ @Nullable
public final String getForPlugin(
String pluginName, String section, String subsection, String name) {
String[] values = getListForPlugin(pluginName, section, subsection, name);
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index b218347..0471b67 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetInfo;
@@ -142,6 +143,7 @@
patchSetInfo = args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
}
+ @Nullable
@Override
public PatchSet updateChangeImpl(ChangeContext ctx) throws NoSuchChangeException, IOException {
if (newCommit == null && toMerge.getStatusCode() == SKIPPED_IDENTICAL_TREE) {
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index 3d38f6c..7aa3716 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.submit;
+import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
@@ -93,7 +94,10 @@
RequestContext old = requestContext.setContext(this);
try {
MergedSender emailSender =
- mergedSenderFactory.create(project, change.getId(), Optional.of(stickyApprovalDiff));
+ mergedSenderFactory.create(
+ project,
+ change.getId(),
+ Optional.ofNullable(Strings.emptyToNull(stickyApprovalDiff)));
if (submitter != null) {
emailSender.setFrom(submitter.getAccountId());
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 27eb0a4..a34aeac 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -929,11 +929,13 @@
}
}
+ @Nullable
private SubmitType getSubmitType(ChangeData cd) {
SubmitTypeRecord str = cd.submitTypeRecord();
return str.isOk() ? str.type : null;
}
+ @Nullable
private OpenRepo openRepo(Project.NameKey project) {
try {
return orm.getRepo(project);
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index cfb2f88..5f58a74 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.exceptions.StorageException;
@@ -238,6 +239,7 @@
acceptMergeTip(args.mergeTip);
}
+ @Nullable
@Override
public PatchSet updateChangeImpl(ChangeContext ctx)
throws NoSuchChangeException, ResourceConflictException, IOException, BadRequestException {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index d06940c..bb2b1a4 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -22,6 +22,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
@@ -158,6 +159,7 @@
}
}
+ @Nullable
private CodeReviewCommit getAlreadyMergedCommit(RepoContext ctx) throws IOException {
CodeReviewCommit tip = args.mergeTip.getInitialTip();
if (tip == null) {
diff --git a/java/com/google/gerrit/server/submit/SubmoduleCommits.java b/java/com/google/gerrit/server/submit/SubmoduleCommits.java
index 37df66b..1fd3ad6 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleCommits.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleCommits.java
@@ -18,6 +18,7 @@
import static java.util.stream.Collectors.toList;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.SubmoduleSubscription;
import com.google.gerrit.exceptions.StorageException;
@@ -212,6 +213,7 @@
return newCommit;
}
+ @Nullable
private RevCommit updateSubmodule(
DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s)
throws SubmoduleConflictException, IOException {
diff --git a/java/com/google/gerrit/server/util/MagicBranch.java b/java/com/google/gerrit/server/util/MagicBranch.java
index 924c288..a5ce108 100644
--- a/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/java/com/google/gerrit/server/util/MagicBranch.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.util;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.entities.Project;
import java.io.IOException;
@@ -38,6 +39,7 @@
}
/** Returns the ref name prefix for a magic branch, {@code null} if the branch is not magic */
+ @Nullable
public static String getMagicRefNamePrefix(String refName) {
if (refName.startsWith(NEW_CHANGE)) {
return NEW_CHANGE;
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index bbc6bf0..83a230d 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//lib:jgit",
"//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
index 97132a3..201a9b7 100644
--- a/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
+++ b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.util.git;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmoduleSubscription;
@@ -65,6 +66,7 @@
return parsedSubscriptions;
}
+ @Nullable
private SubmoduleSubscription parse(String id) {
final String url = config.getString("submodule", id, "url");
final String path = config.getString("submodule", id, "path");
diff --git a/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java b/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java
index f8ab90e..b9ca79c 100644
--- a/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java
+++ b/java/com/google/gerrit/sshd/ChannelIdTrackingUnknownChannelReferenceHandler.java
@@ -45,7 +45,7 @@
public class ChannelIdTrackingUnknownChannelReferenceHandler
extends DefaultUnknownChannelReferenceHandler implements ChannelListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static final AttributeKey<Integer> LAST_CHANNEL_ID_KEY = new AttributeKey<>();
+ public static final AttributeKey<Long> LAST_CHANNEL_ID_KEY = new AttributeKey<>();
public static final ChannelIdTrackingUnknownChannelReferenceHandler TRACKER =
new ChannelIdTrackingUnknownChannelReferenceHandler();
@@ -56,9 +56,9 @@
@Override
public void channelInitialized(Channel channel) {
- int channelId = channel.getId();
+ long channelId = channel.getChannelId();
Session session = channel.getSession();
- Integer lastTracked = session.setAttribute(LAST_CHANNEL_ID_KEY, channelId);
+ Long lastTracked = session.setAttribute(LAST_CHANNEL_ID_KEY, channelId);
logger.atFine().log(
"channelInitialized(%s) updated last tracked channel ID %s => %s",
channel, lastTracked, channelId);
@@ -66,9 +66,9 @@
@Override
public Channel handleUnknownChannelCommand(
- ConnectionService service, byte cmd, int channelId, Buffer buffer) throws IOException {
+ ConnectionService service, byte cmd, long channelId, Buffer buffer) throws IOException {
Session session = service.getSession();
- Integer lastTracked = session.getAttribute(LAST_CHANNEL_ID_KEY);
+ Long lastTracked = session.getAttribute(LAST_CHANNEL_ID_KEY);
if ((lastTracked != null) && (channelId <= lastTracked.intValue())) {
// Use TRACE level in order to avoid messages flooding
logger.atFinest().log(
diff --git a/java/com/google/gerrit/sshd/Commands.java b/java/com/google/gerrit/sshd/Commands.java
index b6d3401..5d641a0 100644
--- a/java/com/google/gerrit/sshd/Commands.java
+++ b/java/com/google/gerrit/sshd/Commands.java
@@ -15,6 +15,7 @@
package com.google.gerrit.sshd;
import com.google.auto.value.AutoAnnotation;
+import com.google.gerrit.common.Nullable;
import com.google.inject.Key;
import java.lang.annotation.Annotation;
import org.apache.sshd.server.command.Command;
@@ -78,6 +79,7 @@
return false;
}
+ @Nullable
static CommandName parentOf(CommandName name) {
if (name instanceof NestedCommandNameImpl) {
return ((NestedCommandNameImpl) name).parent;
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 6997d96..401d31e 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -22,6 +22,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.account.AccountSshKey;
@@ -169,6 +170,7 @@
return p.keys;
}
+ @Nullable
private SshKeyCacheEntry find(Iterable<SshKeyCacheEntry> keyList, PublicKey suppliedKey) {
for (SshKeyCacheEntry k : keyList) {
if (k.match(suppliedKey)) {
diff --git a/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index 5b6d8f9..7adcd24 100644
--- a/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -19,6 +19,7 @@
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.Export;
import com.google.gerrit.server.plugins.InvalidPluginException;
import com.google.gerrit.server.plugins.ModuleGenerator;
@@ -84,6 +85,7 @@
listeners.put(tl, clazz);
}
+ @Nullable
@Override
public Module create() throws InvalidPluginException {
checkState(command != null, "pluginName must be provided");
diff --git a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 6e8590c..8711fe6 100644
--- a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -15,6 +15,7 @@
package com.google.gerrit.sshd;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.plugins.Plugin;
@@ -57,6 +58,7 @@
}
}
+ @Nullable
private Provider<Command> load(Plugin plugin) {
if (plugin.getSshInjector() != null) {
Key<Command> key = Commands.key(plugin.getName());
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 4da55e2..2e29203 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -17,6 +17,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -87,6 +88,7 @@
}
}
+ @Nullable
private String readSshKey() throws IOException {
if (sshKey == null) {
return null;
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 4254e5b..00361ad 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -134,7 +134,9 @@
switch (task.state) {
case DONE:
case CANCELLED:
+ case STARTING:
case RUNNING:
+ case STOPPING:
case READY:
start = format(task.state);
break;
@@ -204,8 +206,12 @@
return "....... done";
case CANCELLED:
return "..... killed";
+ case STOPPING:
+ return "... stopping";
case RUNNING:
return "";
+ case STARTING:
+ return "starting ...";
case READY:
return "waiting ....";
case SLEEPING:
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 1afacab..aadf6d4 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -19,7 +19,7 @@
import static com.google.inject.Scopes.SINGLETON;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
@@ -82,6 +82,7 @@
import com.google.gerrit.server.git.PerThreadRequestScope;
import com.google.gerrit.server.git.SearchingChangeCacheImpl.SearchingChangeCacheImplModule;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
import com.google.gerrit.server.group.testing.TestGroupBackend;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.server.index.account.AllAccountsIndexer;
@@ -198,6 +199,7 @@
install(new AuditModule());
install(new SubscriptionGraphModule());
install(new SuperprojectUpdateSubmissionListenerModule());
+ install(new WorkQueueModule());
bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
@@ -315,9 +317,9 @@
@Provides
@Singleton
@GerritImportedServerIds
- public ImmutableList<String> createImportedServerIds() {
- ImmutableList<String> serverIds =
- ImmutableList.copyOf(
+ public ImmutableSet<String> createImportedServerIds() {
+ ImmutableSet<String> serverIds =
+ ImmutableSet.copyOf(
cfg.getStringList(
GerritServerIdProvider.SECTION, null, GerritImportedServerIdsProvider.KEY));
return serverIds;
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 7c42797..a37c027 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -44,6 +44,7 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.StringWriter;
@@ -567,6 +568,7 @@
* and it needed to be exposed.
*/
@SuppressWarnings("rawtypes")
+ @Nullable
public OptionHandler findOptionByName(String name) {
for (OptionHandler h : optionsList) {
if (h.option instanceof NamedOptionDef) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index c2b779b..111f8c9 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -78,6 +78,7 @@
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
import com.google.gerrit.common.Nullable;
@@ -130,10 +131,12 @@
import com.google.gerrit.httpd.CacheBasedWebSession;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountProperties;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -144,6 +147,7 @@
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.testing.TestGroupBackend;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.index.account.StalenessChecker;
import com.google.gerrit.server.notedb.Sequences;
@@ -175,6 +179,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
@@ -241,6 +246,7 @@
@Inject private ExternalIdKeyFactory externalIdKeyFactory;
@Inject private ExternalIdFactory externalIdFactory;
@Inject private AuthConfig authConfig;
+ @Inject private AccountControl.Factory accountControlFactory;
@Inject protected Emails emails;
@@ -2064,6 +2070,7 @@
return newEmailInput(email, true);
}
+ @Nullable
private String getMetaId(Account.Id accountId) throws IOException {
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo);
@@ -3117,6 +3124,139 @@
.isNotEqualTo(updatedUserState.account().metaId());
}
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void accountsCanSeeEachOtherThroughASharedExternalGroupOnlyWhenTheGroupIsMentionedInAcls()
+ throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ // user and user2 cannot see each other because they do not share a Gerrit internal group
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user2.id())).canSee(user.id()))
+ .isFalse();
+
+ // Configure an external group backend that has a single group that contains all users.
+ TestGroupBackend testGroupBackend = createTestGroupBackendWithAllUsersGroup("AllUsers");
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(testGroupBackend)) {
+ // user and user2 cannot see each other although the external AllUsers group contains both
+ // users. That's because this group is not detected as relevant and hence its memberships are
+ // not checked.
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user2.id())).canSee(user.id()))
+ .isFalse();
+
+ // Add ACL for the external group.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Code-Review")
+ .range(0, 1)
+ .ref("refs/heads/*")
+ .group(AccountGroup.uuid(TestGroupBackend.PREFIX + "AllUsers"))
+ .build())
+ .update();
+
+ // user and user2 can now see each other because the external AllUsers group that contains
+ // both users is guessed as relevant now that permissions are assigned to this group.
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isTrue();
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user2.id())).canSee(user.id()))
+ .isTrue();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ @GerritConfig(name = "groups.relevantGroup", value = "testbackend:AllUsers")
+ public void accountsCanSeeEachOtherThroughASharedExternalGroupThatIsConfiguredAsRelevant()
+ throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ // user and user2 cannot see each other because they do not share a Gerrit internal group
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user2.id())).canSee(user.id()))
+ .isFalse();
+
+ // Check that the configured relevant group is included into the guessed groups.
+ assertThat(projectCache.guessRelevantGroupUUIDs())
+ .contains(AccountGroup.uuid("testbackend:AllUsers"));
+
+ // Configure an external group backend that has a single group that contains all users.
+ TestGroupBackend testGroupBackend = createTestGroupBackendWithAllUsersGroup("AllUsers");
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(testGroupBackend)) {
+ // user and user2 can see each other since the external AllUsers that contains both users has
+ // been configured as a relevant group.
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isTrue();
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user2.id())).canSee(user.id()))
+ .isTrue();
+ }
+ }
+
+ private TestGroupBackend createTestGroupBackendWithAllUsersGroup(String nameOfAllUsersGroup)
+ throws IOException {
+ TestGroupBackend testGroupBackend = new TestGroupBackend();
+
+ AccountGroup.UUID allUsersGroupUuid =
+ testGroupBackend.create(nameOfAllUsersGroup).getGroupUUID();
+
+ GroupMembership testGroupMembership =
+ new GroupMembership() {
+ @Override
+ public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupUuids) {
+ return StreamSupport.stream(groupUuids.spliterator(), /* parallel= */ false)
+ .filter(this::contains)
+ .collect(toSet());
+ }
+
+ @Override
+ public Set<AccountGroup.UUID> getKnownGroups() {
+ // Typically for external group backends it's too expensive to query all groups that the
+ // user is a member of. Instead limit the group membership check to groups that are
+ // guessed to be relevant.
+ return projectCache.guessRelevantGroupUUIDs().stream()
+ // filter out groups of other group backends and groups of this group backend that
+ // don't exist
+ .filter(
+ uuid -> testGroupBackend.handles(uuid) && testGroupBackend.get(uuid) != null)
+ .collect(toImmutableSet());
+ }
+
+ @Override
+ public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupUuids) {
+ return StreamSupport.stream(groupUuids.spliterator(), /* parallel= */ false)
+ .anyMatch(this::contains);
+ }
+
+ @Override
+ public boolean contains(AccountGroup.UUID groupUuid) {
+ return allUsersGroupUuid.equals(groupUuid);
+ }
+ };
+
+ accounts
+ .allIds()
+ .forEach(accountId -> testGroupBackend.setMembershipsOf(accountId, testGroupMembership));
+
+ return testGroupBackend;
+ }
+
private void assertExternalIds(Account.Id accountId, ImmutableSet<String> extIds)
throws Exception {
assertThat(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
new file mode 100644
index 0000000..898e1ff
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -0,0 +1,409 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchPatchSetInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.common.testing.GitPersonSubject;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.api.errors.PatchApplyException;
+import org.eclipse.jgit.util.Base64;
+import org.junit.Test;
+
+public class ApplyPatchIT extends AbstractDaemonTest {
+
+ private static final String COMMIT_MESSAGE = "Applying patch";
+ private static final String DESTINATION_BRANCH = "destBranch";
+
+ private static final String ADDED_FILE_NAME = "a_new_file.txt";
+ private static final String ADDED_FILE_CONTENT = "First added line\nSecond added line\n";
+ private static final String ADDED_FILE_DIFF =
+ "diff --git a/a_new_file.txt b/a_new_file.txt\n"
+ + "new file mode 100644\n"
+ + "--- /dev/null\n"
+ + "+++ b/a_new_file.txt\n"
+ + "@@ -0,0 +1,2 @@\n"
+ + "+First added line\n"
+ + "+Second added line\n";
+
+ @Inject private ProjectOperations projectOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void applyAddedFilePatch_success() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+
+ ChangeInfo result = applyPatch(in);
+
+ DiffInfo diff = fetchDiffForFile(result, ADDED_FILE_NAME);
+ assertDiffForNewFile(diff, result.currentRevision, ADDED_FILE_NAME, ADDED_FILE_CONTENT);
+ }
+
+ private static final String MODIFIED_FILE_NAME = "modified_file.txt";
+ private static final String MODIFIED_FILE_ORIGINAL_CONTENT =
+ "First original line\nSecond original line";
+ private static final String MODIFIED_FILE_NEW_CONTENT = "Modified line\n";
+ private static final String MODIFIED_FILE_DIFF =
+ "diff --git a/modified_file.txt b/modified_file.txt\n"
+ + "new file mode 100644\n"
+ + "--- a/modified_file.txt\n"
+ + "+++ b/modified_file.txt\n"
+ + "@@ -1,2 +1 @@\n"
+ + "-First original line\n"
+ + "-Second original line\n"
+ + "+Modified line\n";
+
+ @Test
+ public void applyModifiedFilePatch_success() throws Exception {
+ initBaseWithFile(MODIFIED_FILE_NAME, MODIFIED_FILE_ORIGINAL_CONTENT);
+ ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
+
+ ChangeInfo result = applyPatch(in);
+
+ DiffInfo diff = fetchDiffForFile(result, MODIFIED_FILE_NAME);
+ assertDiffForFullyModifiedFile(
+ diff,
+ result.currentRevision,
+ MODIFIED_FILE_NAME,
+ MODIFIED_FILE_ORIGINAL_CONTENT,
+ MODIFIED_FILE_NEW_CONTENT);
+ }
+
+ @Test
+ public void applyDeletedFilePatch_success() throws Exception {
+ final String deletedFileName = "deleted_file.txt";
+ final String deletedFileOriginalContent = "content to be deleted.\n";
+ final String deletedFileDiff =
+ "diff --git a/deleted_file.txt b/deleted_file.txt\n"
+ + "--- a/deleted_file.txt\n"
+ + "+++ /dev/null\n";
+ initBaseWithFile(deletedFileName, deletedFileOriginalContent);
+ ApplyPatchPatchSetInput in = buildInput(deletedFileDiff);
+
+ ChangeInfo result = applyPatch(in);
+
+ DiffInfo diff = fetchDiffForFile(result, deletedFileName);
+ assertDiffForDeletedFile(diff, deletedFileName, deletedFileOriginalContent);
+ }
+
+ @Test
+ public void applyRenamedFilePatch_success() throws Exception {
+ final String renamedFileOriginalName = "renamed_file_origin.txt";
+ final String renamedFileNewName = "renamed_file_new.txt";
+ final String renamedFileDiff =
+ "diff --git a/renamed_file_origin.txt b/renamed_file_new.txt\n"
+ + "rename from renamed_file_origin.txt\n"
+ + "rename to renamed_file_new.txt\n"
+ + "--- a/renamed_file_origin.txt\n"
+ + "+++ b/renamed_file_new.txt\n"
+ + "@@ -1,2 +1 @@\n"
+ + "-First original line\n"
+ + "-Second original line\n"
+ + "+Modified line\n";
+ initBaseWithFile(renamedFileOriginalName, MODIFIED_FILE_ORIGINAL_CONTENT);
+ ApplyPatchPatchSetInput in = buildInput(renamedFileDiff);
+
+ ChangeInfo result = applyPatch(in);
+
+ DiffInfo originalFileDiff = fetchDiffForFile(result, renamedFileOriginalName);
+ assertDiffForDeletedFile(
+ originalFileDiff, renamedFileOriginalName, MODIFIED_FILE_ORIGINAL_CONTENT);
+ DiffInfo newFileDiff = fetchDiffForFile(result, renamedFileNewName);
+ assertDiffForNewFile(
+ newFileDiff, result.currentRevision, renamedFileNewName, MODIFIED_FILE_NEW_CONTENT);
+ }
+
+ @Test
+ public void applyGerritBasedPatchWithSingleFile_success() throws Exception {
+ String head = getHead(repo(), HEAD).name();
+ createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+ PushOneCommit.Result baseCommit = createChange("Add file", ADDED_FILE_NAME, ADDED_FILE_CONTENT);
+ baseCommit.assertOkStatus();
+ BinaryResult originalPatch = gApi.changes().id(baseCommit.getChangeId()).current().patch();
+ ApplyPatchPatchSetInput in = buildInput(originalPatch.asString());
+ createBranchWithRevision(BranchNameKey.create(project, DESTINATION_BRANCH), head);
+
+ ChangeInfo result = applyPatch(in);
+
+ BinaryResult resultPatch = gApi.changes().id(result.id).current().patch();
+ assertThat(removeHeader(resultPatch)).isEqualTo(removeHeader(originalPatch));
+ }
+
+ @Test
+ public void applyGerritBasedPatchWithMultipleFiles_success() throws Exception {
+ PushOneCommit.Result commonBaseCommit =
+ createChange("File for modification", MODIFIED_FILE_NAME, MODIFIED_FILE_ORIGINAL_CONTENT);
+ commonBaseCommit.assertOkStatus();
+ String head = getHead(repo(), HEAD).name();
+ createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+ PushOneCommit.Result commitToPatch =
+ createChange("Add file", ADDED_FILE_NAME, ADDED_FILE_CONTENT);
+ amendChange(
+ commitToPatch.getChangeId(), "Modify file", MODIFIED_FILE_NAME, MODIFIED_FILE_NEW_CONTENT);
+ commitToPatch.assertOkStatus();
+ BinaryResult originalPatch = gApi.changes().id(commitToPatch.getChangeId()).current().patch();
+ ApplyPatchPatchSetInput in = buildInput(originalPatch.asString());
+ createBranchWithRevision(BranchNameKey.create(project, DESTINATION_BRANCH), head);
+
+ ChangeInfo result = applyPatch(in);
+
+ BinaryResult resultPatch = gApi.changes().id(result.id).current().patch();
+ assertThat(removeHeader(resultPatch)).isEqualTo(removeHeader(originalPatch));
+ }
+
+ @Test
+ public void applyGerritBasedPatchUsingRest_success() throws Exception {
+ String head = getHead(repo(), HEAD).name();
+ createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+ PushOneCommit.Result baseCommit = createChange("Add file", ADDED_FILE_NAME, ADDED_FILE_CONTENT);
+ baseCommit.assertOkStatus();
+ createBranchWithRevision(BranchNameKey.create(project, DESTINATION_BRANCH), head);
+ RestResponse patchResp =
+ userRestSession.get("/changes/" + baseCommit.getChangeId() + "/revisions/current/patch");
+ patchResp.assertOK();
+ String originalPatch = new String(Base64.decode(patchResp.getEntityContent()), UTF_8);
+ ApplyPatchPatchSetInput in = buildInput(originalPatch);
+ PushOneCommit.Result destChange = createChange();
+
+ RestResponse resp =
+ adminRestSession.post("/changes/" + destChange.getChangeId() + "/patch:apply", in);
+
+ resp.assertOK();
+ BinaryResult resultPatch = gApi.changes().id(destChange.getChangeId()).current().patch();
+ assertThat(removeHeader(resultPatch)).isEqualTo(removeHeader(originalPatch));
+ }
+
+ @Test
+ public void applyPatchWithConflict_fails() throws Exception {
+ initBaseWithFile(MODIFIED_FILE_NAME, "Unexpected base content");
+ ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
+
+ Throwable error = assertThrows(RestApiException.class, () -> applyPatch(in));
+
+ assertThat(error).hasMessageThat().contains("Cannot apply patch");
+ assertThat(error).hasCauseThat().isInstanceOf(PatchApplyException.class);
+ assertThat(error).hasCauseThat().hasMessageThat().contains("Cannot apply: HunkHeader");
+ }
+
+ @Test
+ public void applyPatchWithoutAddPatchSetPermissions_fails() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .remove(
+ permissionKey(Permission.ADD_PATCH_SET)
+ .ref("refs/for/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+ PushOneCommit.Result destChange = createChange("dest change", "a file", "with content");
+ // Add-patch is always allowed for the change owner, so we need to use another account.
+ requestScopeOperations.setApiUser(accountCreator.user2().id());
+
+ Throwable error =
+ assertThrows(
+ AuthException.class, () -> gApi.changes().id(destChange.getChangeId()).applyPatch(in));
+
+ assertThat(error).hasMessageThat().contains("patch set");
+ }
+
+ @Test
+ public void applyPatchWithCustomMessage_success() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.commitMessage = "custom commit message";
+
+ ChangeInfo result = applyPatch(in);
+
+ assertThat(gApi.changes().id(result.id).current().commit(false).message)
+ .contains(in.commitMessage);
+ }
+
+ @Test
+ public void applyPatchWithBaseCommit_success() throws Exception {
+ PushOneCommit.Result baseCommit =
+ createChange("base commit", MODIFIED_FILE_NAME, MODIFIED_FILE_ORIGINAL_CONTENT);
+ baseCommit.assertOkStatus();
+ PushOneCommit.Result ignoredCommit =
+ createChange("Ignored file modification", MODIFIED_FILE_NAME, "Ignored file modification");
+ ignoredCommit.assertOkStatus();
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
+ in.base = baseCommit.getCommit().getName();
+
+ ChangeInfo result = applyPatch(in);
+
+ assertThat(gApi.changes().id(result.id).current().commit(false).parents.get(0).commit)
+ .isEqualTo(in.base);
+ }
+
+ @Test
+ public void applyPatchWithDefaultAuthor_success() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+
+ ChangeInfo result = applyPatch(in);
+
+ GitPerson person = gApi.changes().id(result.id).current().commit(false).author;
+ GitPersonSubject.assertThat(person).email().isEqualTo(admin.email());
+ }
+
+ @Test
+ public void applyPatchWithAuthorOverrideMissingEmail_throwsIllegalArgument() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.author = new AccountInput();
+ in.author.name = "name";
+
+ Throwable error = assertThrows(IllegalArgumentException.class, () -> applyPatch(in));
+
+ assertThat(error).hasMessageThat().contains("E-mail");
+ }
+
+ @Test
+ public void applyPatchWithAuthorOverrideMissingName_throwsIllegalArgument() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.author = new AccountInput();
+ in.author.name = null;
+ in.author.email = "gerritlessjane@invalid";
+
+ Throwable error = assertThrows(IllegalArgumentException.class, () -> applyPatch(in));
+
+ assertThat(error).hasMessageThat().contains("Name");
+ }
+
+ @Test
+ public void applyPatchWithAuthorOverride_success() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.author = new AccountInput();
+ in.author.email = "gerritlessjane@invalid";
+ // This is an email address that doesn't exist as account on the Gerrit server.
+ in.author.name = "Gerritless Jane";
+
+ ChangeInfo result = applyPatch(in);
+
+ RevisionApi rApi = gApi.changes().id(result.id).current();
+ GitPerson author = rApi.commit(false).author;
+ GitPersonSubject.assertThat(author).email().isEqualTo(in.author.email);
+ GitPersonSubject.assertThat(author).name().isEqualTo(in.author.name);
+ GitPerson committer = rApi.commit(false).committer;
+ GitPersonSubject.assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
+ }
+
+ @Test
+ public void applyPatchWithAuthorWithoutPermissions_fails() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.author = new AccountInput();
+ in.author.name = "Jane";
+ in.author.email = "jane@invalid";
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ Throwable error = assertThrows(ResourceConflictException.class, () -> applyPatch(in));
+
+ assertThat(error).hasMessageThat().contains("forge author");
+ }
+
+ @Test
+ public void applyPatchWithSelfAsForgedAuthor_success() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.author = new AccountInput();
+ in.author.name = admin.fullName();
+ in.author.email = admin.email();
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ ChangeInfo result = applyPatch(in);
+
+ GitPerson person = gApi.changes().id(result.id).current().commit(false).author;
+ GitPersonSubject.assertThat(person).email().isEqualTo(admin.email());
+ }
+
+ private void initDestBranch() throws Exception {
+ String head = getHead(repo(), HEAD).name();
+ createBranchWithRevision(BranchNameKey.create(project, ApplyPatchIT.DESTINATION_BRANCH), head);
+ }
+
+ private void initBaseWithFile(String fileName, String fileContent) throws Exception {
+ PushOneCommit.Result baseCommit =
+ createChange("Add original file: " + fileName, fileName, fileContent);
+ baseCommit.assertOkStatus();
+ initDestBranch();
+ }
+
+ private ApplyPatchPatchSetInput buildInput(String patch) {
+ ApplyPatchPatchSetInput in = new ApplyPatchPatchSetInput();
+ in.patch = new ApplyPatchInput();
+ in.patch.patch = patch;
+ return in;
+ }
+
+ private ChangeInfo applyPatch(ApplyPatchPatchSetInput input) throws RestApiException {
+ return gApi.changes()
+ .create(new ChangeInput(project.get(), DESTINATION_BRANCH, COMMIT_MESSAGE))
+ .applyPatch(input);
+ }
+
+ private DiffInfo fetchDiffForFile(ChangeInfo result, String fileName) throws RestApiException {
+ return gApi.changes().id(result.id).current().file(fileName).diff();
+ }
+
+ private String removeHeader(BinaryResult bin) throws IOException {
+ return removeHeader(bin.asString());
+ }
+
+ private String removeHeader(String s) {
+ return s.substring(s.indexOf("\ndiff --git"), s.length() - 1);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index bd156f4..aa174c6 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -120,6 +120,7 @@
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
+import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewResult;
@@ -419,6 +420,31 @@
}
@Test
+ public void setReadyForReviewSendsNotificationsForRevertChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+ RevertInput in = new RevertInput();
+ in.workInProgress = true;
+ String changeId = gApi.changes().id(r.getChangeId()).revert(in).get().changeId;
+ requestScopeOperations.setApiUser(admin.id());
+
+ gApi.changes().id(changeId).setReadyForReview();
+
+ assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+ // expected messages on source change:
+ // 1. Uploaded patch set 1.
+ // 2. Patch Set 1: Code-Review+2
+ // 3. Change has been successfully merged by Administrator
+ // 4. Patch Set 1: Reverted
+ List<ChangeMessageInfo> sourceMessages =
+ new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
+ assertThat(sourceMessages).hasSize(4);
+ String expectedMessage = String.format("Created a revert of this change as I%s", changeId);
+ assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
+ }
+
+ @Test
public void hasReviewStarted() throws Exception {
PushOneCommit.Result r = createWorkInProgressChange();
String changeId = r.getChangeId();
@@ -1049,6 +1075,69 @@
}
@Test
+ public void rebaseOutdatedPatchSet() throws Exception {
+ String fileName1 = "a.txt";
+ String fileContent1 = "some content";
+ String fileName2 = "b.txt";
+ String fileContent2Ps1 = "foo";
+ String fileContent2Ps2 = "foo/bar";
+
+ // Create two changes both with the same parent touching disjunct files
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName1, fileContent1)
+ .to("refs/for/master");
+ r.assertOkStatus();
+ String changeId1 = r.getChangeId();
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName2, fileContent2Ps1);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ String changeId2 = r2.getChangeId();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(changeId1).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Amend the second change so that it has 2 patch sets
+ amendChange(
+ changeId2,
+ "refs/for/master",
+ admin,
+ testRepo,
+ PushOneCommit.SUBJECT,
+ fileName2,
+ fileContent2Ps2)
+ .assertOkStatus();
+ ChangeInfo changeInfo2 = gApi.changes().id(changeId2).get();
+ assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(2);
+
+ // Rebase the first patch set of the second change
+ gApi.changes().id(changeId2).revision(1).rebase();
+
+ // Second change should have 3 patch sets
+ changeInfo2 = gApi.changes().id(changeId2).get();
+ assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(3);
+
+ // ... and the committer and description should be correct
+ ChangeInfo info = gApi.changes().id(changeId2).get(CURRENT_REVISION, CURRENT_COMMIT);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo(admin.fullName());
+ assertThat(committer.email).isEqualTo(admin.email());
+ String description = info.revisions.get(info.currentRevision).description;
+ assertThat(description).isEqualTo("Rebase");
+
+ // ... and the file contents should match with patch set 1 based on change1
+ assertThat(gApi.changes().id(changeId2).current().file(fileName1).content().asString())
+ .isEqualTo(fileContent1);
+ assertThat(gApi.changes().id(changeId2).current().file(fileName2).content().asString())
+ .isEqualTo(fileContent2Ps1);
+ }
+
+ @Test
public void deleteNewChangeAsAdmin() throws Exception {
deleteChangeAsUser(admin, admin);
}
@@ -3413,6 +3502,18 @@
}
@Test
+ public void stableRevisionSort() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ r1.assertOkStatus();
+ PushOneCommit.Result r2 = amendChange(r1.getChangeId());
+ r2.assertOkStatus();
+
+ ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(ALL_REVISIONS, CURRENT_REVISION);
+ assertThat(actual.revisions).hasSize(2);
+ assertThat(actual.revisions.values().stream().map(r -> r._number)).isInOrder();
+ }
+
+ @Test
public void defaultSearchDoesNotTouchDatabase() throws Exception {
requestScopeOperations.setApiUser(admin.id());
PushOneCommit.Result r1 = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 9de33be..527253c 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -26,6 +26,7 @@
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -267,10 +268,19 @@
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
-
RevertInput in = createWipRevertInput();
+
ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(in).get();
+
assertThat(revertChange.workInProgress).isTrue();
+ // expected messages on source change:
+ // 1. Uploaded patch set 1.
+ // 2. Patch Set 1: Code-Review+2
+ // 3. Change has been successfully merged by Administrator
+ // No "reverted" message is expected.
+ List<ChangeMessageInfo> sourceMessages =
+ new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
+ assertThat(sourceMessages).hasSize(3);
}
@Test
@@ -365,14 +375,14 @@
}
@Test
- public void revertNotificationsSupressedOnWip() throws Exception {
+ public void revertNotificationsSuppressedOnWip() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).addReviewer(user.email());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
sender.clear();
- // If notify input not specified, the endpoint overrides it to OWNER
+ // If notify input not specified, the endpoint overrides it to NONE
RevertInput revertInput = createWipRevertInput();
revertInput.notify = null;
gApi.changes().id(r.getChangeId()).revert(revertInput).get();
@@ -424,6 +434,46 @@
}
@Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void revertWithNonVisibleUsers() throws Exception {
+ // Define readable names for the users we use in this test.
+ TestAccount reverter = user;
+ TestAccount changeOwner = admin; // must be admin, since admin cloned testRepo
+ TestAccount reviewer = accountCreator.user2();
+ TestAccount cc =
+ accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
+
+ // Check that the reverter can neither see the changeOwner, the reviewer nor the cc.
+ requestScopeOperations.setApiUser(reverter.id());
+ assertThatAccountIsNotVisible(changeOwner, reviewer, cc);
+
+ // Create the change.
+ requestScopeOperations.setApiUser(changeOwner.id());
+ PushOneCommit.Result r = createChange();
+
+ // Add reviewer and cc.
+ ReviewInput reviewerInput = ReviewInput.approve();
+ reviewerInput.reviewer(reviewer.email());
+ reviewerInput.cc(cc.email());
+ gApi.changes().id(r.getChangeId()).current().review(reviewerInput);
+
+ // Approve and submit the change.
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ // Revert the change.
+ requestScopeOperations.setApiUser(reverter.id());
+ String revertChangeId = gApi.changes().id(r.getChangeId()).revert().get().id;
+
+ // Revert doesn't check the reviewer/CC visibility. Since the reverter can see the reverted
+ // change, they can also see its reviewers/CCs. This means preserving them on the revert change
+ // doesn't expose their account existence and it's OK to keep them even if their accounts are
+ // not visible to the reverter.
+ assertReviewers(revertChangeId, changeOwner, reviewer);
+ assertCcs(revertChangeId, cc);
+ }
+
+ @Test
@TestProjectInput(createEmptyCommit = false)
public void revertInitialCommit() throws Exception {
PushOneCommit.Result r = createChange();
@@ -765,8 +815,7 @@
gApi.changes().id(secondResult).current().submit();
sender.clear();
- RevertInput revertInput = new RevertInput();
- revertInput.workInProgress = true;
+ RevertInput revertInput = createWipRevertInput();
revertInput.notify = NotifyHandling.NONE;
gApi.changes().id(secondResult).revertSubmission(revertInput);
assertThat(sender.getMessages()).isEmpty();
@@ -788,9 +837,14 @@
// If notify handling is specified, it will be used by the API
RevertInput revertInput = createWipRevertInput();
revertInput.notify = NotifyHandling.ALL;
- gApi.changes().id(changeId2).revertSubmission(revertInput);
+ RevertSubmissionInfo revertChanges = gApi.changes().id(changeId2).revertSubmission(revertInput);
- assertThat(sender.getMessages()).hasSize(4);
+ assertThat(revertChanges.revertChanges).hasSize(2);
+ assertThat(sender.getMessages()).hasSize(2);
+ assertThat(sender.getMessages(revertChanges.revertChanges.get(0).changeId, "newchange"))
+ .hasSize(1);
+ assertThat(sender.getMessages(revertChanges.revertChanges.get(1).changeId, "newchange"))
+ .hasSize(1);
}
@Test
@@ -801,17 +855,23 @@
String changeId2 = createChange("second change", "b.txt", "other").getChangeId();
approve(changeId2);
gApi.changes().id(changeId2).addReviewer(user.email());
-
gApi.changes().id(changeId2).current().submit();
-
sender.clear();
-
RevertInput revertInput = createWipRevertInput();
+
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(changeId2).revertSubmission(revertInput);
assertThat(revertSubmissionInfo.revertChanges.stream().allMatch(r -> r.workInProgress))
.isTrue();
+
+ // expected messages on source change:
+ // 1. Uploaded patch set 1.
+ // 2. Patch Set 1: Code-Review+2
+ // 3. Change has been successfully merged by Administrator
+ // No "reverted" message is expected.
+ assertThat(gApi.changes().id(changeId1).get().messages).hasSize(3);
+ assertThat(gApi.changes().id(changeId2).get().messages).hasSize(3);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
index a443739..56e23a4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
@@ -19,15 +19,20 @@
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
@@ -37,6 +42,10 @@
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -198,6 +207,130 @@
"distinctvoters:\"[Code-Review,Custom-Label,Custom-Label2],value=1,count>2\"", c1);
}
+ @Test
+ public void hasSubmoduleUpdate_withSubmoduleChangeInParent1() throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+ PushOneCommit.Result r1 = createGitSubmoduleCommit("refs/for/master");
+ testRepo.reset(initial);
+ PushOneCommit.Result r2 = createNormalCommit("refs/for/master", "file1");
+ PushOneCommit.Result merge =
+ createMergeCommitChange(
+ "refs/for/master",
+ r1.getCommit(),
+ r2.getCommit(),
+ mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
+
+ assertNotMatching("has:submodule-update,base=1", merge.getChange().getId());
+ assertMatching("has:submodule-update,base=2", merge.getChange().getId());
+ assertNotMatching("has:submodule-update", merge.getChange().getId());
+ }
+
+ @Test
+ public void hasSubmoduleUpdate_withSubmoduleChangeInParent2() throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+ PushOneCommit.Result r1 = createNormalCommit("refs/for/master", "file1");
+ testRepo.reset(initial);
+ PushOneCommit.Result r2 = createGitSubmoduleCommit("refs/for/master");
+ PushOneCommit.Result merge =
+ createMergeCommitChange(
+ "refs/for/master",
+ r1.getCommit(),
+ r2.getCommit(),
+ mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
+
+ assertMatching("has:submodule-update,base=1", merge.getChange().getId());
+ assertNotMatching("has:submodule-update,base=2", merge.getChange().getId());
+ assertNotMatching("has:submodule-update", merge.getChange().getId());
+ }
+
+ @Test
+ public void hasSubmoduleUpdate_withoutSubmoduleChange_doesNotMatch() throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+ PushOneCommit.Result r1 = createNormalCommit("refs/for/master", "file1");
+ testRepo.reset(initial);
+ PushOneCommit.Result r2 = createNormalCommit("refs/for/master", "file2");
+ PushOneCommit.Result merge =
+ createMergeCommitChange(
+ "refs/for/master",
+ r1.getCommit(),
+ r2.getCommit(),
+ mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
+
+ assertNotMatching("has:submodule-update,base=1", merge.getChange().getId());
+ assertNotMatching("has:submodule-update,base=2", merge.getChange().getId());
+ assertNotMatching("has:submodule-update", merge.getChange().getId());
+ }
+
+ @Test
+ public void hasSubmoduleUpdate_withBaseParamGreaterThanParentCount_doesNotMatch()
+ throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+ PushOneCommit.Result r1 = createNormalCommit("refs/for/master", "file1");
+ testRepo.reset(initial);
+ PushOneCommit.Result r2 = createGitSubmoduleCommit("refs/for/master");
+ PushOneCommit.Result merge =
+ createMergeCommitChange(
+ "refs/for/master",
+ r1.getCommit(),
+ r2.getCommit(),
+ mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
+
+ assertNotMatching("has:submodule-update,base=3", merge.getChange().getId());
+ }
+
+ @Test
+ public void hasSubmoduleUpdate_withWrongArgs_throws() {
+ assertError(
+ "has:submodule-update,base=xyz",
+ changeOperations.newChange().project(project).create(),
+ "failed to parse the parent number xyz: For input string: \"xyz\"");
+ assertError(
+ "has:submodule-update,base=1,arg=foo",
+ changeOperations.newChange().project(project).create(),
+ "wrong number of arguments for the has:submodule-update operator");
+ assertError(
+ "has:submodule-update,base",
+ changeOperations.newChange().project(project).create(),
+ "unexpected base value format");
+ }
+
+ private PushOneCommit.Result createGitSubmoduleCommit(String ref) throws Exception {
+ return pushFactory
+ .create(admin.newIdent(), testRepo, "subject", ImmutableMap.of())
+ .addGitSubmodule(
+ "modules/module-a", ObjectId.fromString("19f1787342cb15d7e82a762f6b494e91ccb4dd34"))
+ .to(ref);
+ }
+
+ private PushOneCommit.Result createNormalCommit(String ref, String fileName) throws Exception {
+ return pushFactory
+ .create(admin.newIdent(), testRepo, "subject", ImmutableMap.of(fileName, fileName))
+ .to(ref);
+ }
+
+ private PushOneCommit.Result createMergeCommitChange(
+ String ref, RevCommit parent1, RevCommit parent2, @Nullable ObjectId treeId)
+ throws Exception {
+ PushOneCommit m =
+ pushFactory
+ .create(admin.newIdent(), testRepo)
+ .setParents(ImmutableList.of(parent1, parent2));
+ if (treeId != null) {
+ m.setTopLevelTreeId(treeId);
+ }
+ PushOneCommit.Result result = m.to(ref);
+ result.assertOkStatus();
+ return result;
+ }
+
+ private ObjectId mergeAndGetTreeId(RevCommit c1, RevCommit c2) throws Exception {
+ ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repo(), true);
+ threeWayMerger.setBase(c1.getParent(0));
+ boolean mergeResult = threeWayMerger.merge(c1, c2);
+ assertThat(mergeResult).isTrue();
+ return threeWayMerger.getResultTreeId();
+ }
+
private void assertMatching(String requirement, Change.Id change) {
assertThat(evaluate(requirement, change).status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
@@ -208,6 +341,12 @@
.isEqualTo(SubmitRequirementExpressionResult.Status.FAIL);
}
+ private void assertError(String requirement, Change.Id change, String errorMessage) {
+ SubmitRequirementExpressionResult result = evaluate(requirement, change);
+ assertThat(result.status()).isEqualTo(SubmitRequirementExpressionResult.Status.ERROR);
+ assertThat(result.errorMessage().get()).isEqualTo(errorMessage);
+ }
+
private SubmitRequirementExpressionResult evaluate(String requirement, Change.Id change) {
ChangeData cd = changeDataFactory.create(project, change);
return submitRequirementsEvaluator.evaluateExpression(
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 18eca27..462d0a5 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -24,6 +24,7 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
import com.google.gerrit.extensions.api.plugins.PluginApi;
@@ -198,6 +199,7 @@
return pluginJarContent(plugin);
}
+ @Nullable
private String pluginVersion(String plugin) {
String name = pluginName(plugin);
if (name.endsWith("empty")) {
@@ -210,6 +212,7 @@
return dash > 0 ? name.substring(dash + 1) : "";
}
+ @Nullable
private String pluginApiVersion(String plugin) {
if (plugin.endsWith("normal.jar")) {
return "2.16.19-SNAPSHOT";
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
index 168819c..52207db 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
@@ -25,6 +25,8 @@
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -36,6 +38,7 @@
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
public class ProjectConfigIT extends AbstractDaemonTest {
@@ -389,6 +392,168 @@
}
@Test
+ public void rejectSubmitRequirement_duplicateDescriptionKeys() throws Exception {
+ fetchRefsMetaConfig();
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Test Change",
+ ProjectConfig.PROJECT_CONFIG,
+ "[submit-requirement \"Foo\"]\n"
+ + " description = description 1\n "
+ + " submittableIf = label:Code-Review=MAX\n"
+ + "[submit-requirement \"Foo\"]\n"
+ + " description = description 2\n");
+ PushOneCommit.Result r = push.to(RefNames.REFS_CONFIG);
+ r.assertErrorStatus(
+ String.format("commit %s: invalid project configuration", abbreviateName(r.getCommit())));
+ r.assertMessage(
+ String.format(
+ "ERROR: commit %s: project.config: multiple definitions of description"
+ + " for submit requirement 'foo'",
+ abbreviateName(r.getCommit())));
+ }
+
+ @Test
+ public void rejectSubmitRequirement_duplicateApplicableIfKeys() throws Exception {
+ fetchRefsMetaConfig();
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Test Change",
+ ProjectConfig.PROJECT_CONFIG,
+ "[submit-requirement \"Foo\"]\n "
+ + " applicableIf = is:true\n "
+ + " submittableIf = label:Code-Review=MAX\n"
+ + "[submit-requirement \"Foo\"]\n"
+ + " applicableIf = is:false\n");
+ PushOneCommit.Result r = push.to(RefNames.REFS_CONFIG);
+ r.assertErrorStatus(
+ String.format("commit %s: invalid project configuration", abbreviateName(r.getCommit())));
+ r.assertMessage(
+ String.format(
+ "ERROR: commit %s: project.config: multiple definitions of applicableif"
+ + " for submit requirement 'foo'",
+ abbreviateName(r.getCommit())));
+ }
+
+ @Test
+ public void rejectSubmitRequirement_duplicateSubmittableIfKeys() throws Exception {
+ fetchRefsMetaConfig();
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Test Change",
+ ProjectConfig.PROJECT_CONFIG,
+ "[submit-requirement \"Foo\"]\n"
+ + " submittableIf = label:Code-Review=MAX\n"
+ + "[submit-requirement \"Foo\"]\n"
+ + " submittableIf = label:Code-Review=MIN\n");
+ PushOneCommit.Result r = push.to(RefNames.REFS_CONFIG);
+ r.assertErrorStatus(
+ String.format("commit %s: invalid project configuration", abbreviateName(r.getCommit())));
+ r.assertMessage(
+ String.format(
+ "ERROR: commit %s: project.config: multiple definitions of submittableif"
+ + " for submit requirement 'foo'",
+ abbreviateName(r.getCommit())));
+ }
+
+ @Test
+ public void rejectSubmitRequirement_duplicateOverrideIfKeys() throws Exception {
+ fetchRefsMetaConfig();
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Test Change",
+ ProjectConfig.PROJECT_CONFIG,
+ "[submit-requirement \"Foo\"]\n"
+ + " overrideIf = is:true\n "
+ + " submittableIf = label:Code-Review=MAX\n"
+ + "[submit-requirement \"Foo\"]\n"
+ + " overrideIf = is:false\n");
+ PushOneCommit.Result r = push.to(RefNames.REFS_CONFIG);
+ r.assertErrorStatus(
+ String.format("commit %s: invalid project configuration", abbreviateName(r.getCommit())));
+ r.assertMessage(
+ String.format(
+ "ERROR: commit %s: project.config: multiple definitions of overrideif"
+ + " for submit requirement 'foo'",
+ abbreviateName(r.getCommit())));
+ }
+
+ @Test
+ public void rejectSubmitRequirement_duplicateCanOverrideInChildProjectsKey() throws Exception {
+ fetchRefsMetaConfig();
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Test Change",
+ ProjectConfig.PROJECT_CONFIG,
+ "[submit-requirement \"Foo\"]\n"
+ + " canOverrideInChildProjects = true\n"
+ + " submittableIf = label:Code-Review=MAX\n"
+ + "[submit-requirement \"Foo\"]\n "
+ + " canOverrideInChildProjects = false\n");
+ PushOneCommit.Result r = push.to(RefNames.REFS_CONFIG);
+ r.assertErrorStatus(
+ String.format("commit %s: invalid project configuration", abbreviateName(r.getCommit())));
+ r.assertMessage(
+ String.format(
+ "ERROR: commit %s: project.config: multiple definitions of canoverrideinchildprojects"
+ + " for submit requirement 'foo'",
+ abbreviateName(r.getCommit())));
+ }
+
+ @Test
+ public void submitRequirementsAreParsed_forExistingDuplicateDefinitions() throws Exception {
+ // Duplicate submit requirement definitions are rejected on config change uploads. For setups
+ // already containing duplicate SR definitions, the server is able to parse the "submit
+ // requirements correctly"
+
+ RevCommit revision;
+ // Commit a change to the project config, bypassing server validation.
+ try (TestRepository<Repository> testRepo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ revision =
+ testRepo
+ .branch(RefNames.REFS_CONFIG)
+ .commit()
+ .add(
+ ProjectConfig.PROJECT_CONFIG,
+ "[submit-requirement \"Foo\"]\n"
+ + " canOverrideInChildProjects = true\n"
+ + " submittableIf = label:Code-Review=MAX\n"
+ + "[submit-requirement \"Foo\"]\n "
+ + " canOverrideInChildProjects = false\n")
+ .parent(projectOperations.project(project).getHead(RefNames.REFS_CONFIG))
+ .create();
+ }
+
+ try (Repository git = repoManager.openRepository(project)) {
+ // Server is able to parse the config.
+ ProjectConfig cfg = projectConfigFactory.create(project);
+ cfg.load(git, revision);
+
+ // One of the two definitions takes precedence and overrides the other.
+ assertThat(cfg.getSubmitRequirementSections())
+ .containsExactly(
+ "Foo",
+ SubmitRequirement.builder()
+ .setName("Foo")
+ .setAllowOverrideInChildProjects(false)
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=MAX"))
+ .build());
+ }
+ }
+
+ @Test
public void testRejectChangingLabelFunction_toMaxWithBlock() throws Exception {
testChangingLabelFunction(
/* initialLabelFunction= */ LabelFunction.NO_BLOCK,
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 1919810..fca2253 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -227,6 +227,66 @@
}
@Test
+ public void fileModeChangeIsIncludedInListFilesDiff() throws Exception {
+ String fileName = "file.txt";
+ PushOneCommit push =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Commit Subject", /* files= */ ImmutableMap.of())
+ .addFile(fileName, "content", /* fileMode= */ 0100644);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ String commitRev1 = gApi.changes().id(result.getChangeId()).get().currentRevision;
+ push =
+ pushFactory
+ .create(admin.newIdent(), testRepo, result.getChangeId())
+ .addFile(fileName, "content", /* fileMode= */ 0100755);
+ result = push.to("refs/for/master");
+ String commitRev2 = gApi.changes().id(result.getChangeId()).get().currentRevision;
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(result.getChangeId()).revision(commitRev2).files(commitRev1);
+
+ assertThat(changedFiles.get(fileName)).oldMode().isEqualTo(0100644);
+ assertThat(changedFiles.get(fileName)).newMode().isEqualTo(0100755);
+ }
+
+ @Test
+ public void fileMode_oldMode_isMissingInListFilesDiff_forAddedFile() throws Exception {
+ String fileName = "file.txt";
+ PushOneCommit push =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Commit Subject", /* files= */ ImmutableMap.of())
+ .addFile(fileName, "content", /* fileMode= */ 0100644);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ String commitRev = gApi.changes().id(result.getChangeId()).get().currentRevision;
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(result.getChangeId()).revision(commitRev).files();
+
+ assertThat(changedFiles.get(fileName)).oldMode().isNull();
+ assertThat(changedFiles.get(fileName)).newMode().isEqualTo(0100644);
+ }
+
+ @Test
+ public void fileMode_newMode_isMissingInListFilesDiff_forDeletedFile() throws Exception {
+ String fileName = "file.txt";
+ PushOneCommit push =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Commit Subject", /* files= */ ImmutableMap.of())
+ .addFile(fileName, "content", /* fileMode= */ 0100644);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ String commitRev1 = gApi.changes().id(result.getChangeId()).get().currentRevision;
+ push = pushFactory.create(admin.newIdent(), testRepo, result.getChangeId()).rmFile(fileName);
+ result = push.to("refs/for/master");
+ String commitRev2 = gApi.changes().id(result.getChangeId()).get().currentRevision;
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(result.getChangeId()).revision(commitRev2).files(commitRev1);
+
+ assertThat(changedFiles.get(fileName)).oldMode().isEqualTo(0100644);
+ assertThat(changedFiles.get(fileName)).newMode().isNull();
+ }
+
+ @Test
public void numberOfLinesInDiffOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
String filePath = "a_new_file.txt";
String fileContent = "Line 1\nLine 2\nLine 3";
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 804516a..0291f33 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -1009,6 +1009,84 @@
}
@Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void cherryPickWithNonVisibleUsers() throws Exception {
+ // Create a target branch for the cherry-pick.
+ createBranch(BranchNameKey.create(project, "stable"));
+
+ // Define readable names for the users we use in this test.
+ TestAccount cherryPicker = user;
+ TestAccount changeOwner = admin;
+ TestAccount reviewer = accountCreator.user2();
+ TestAccount cc =
+ accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
+ TestAccount authorCommitter =
+ accountCreator.create("user4", "user4@example.com", "User4", /* displayName= */ null);
+
+ // Check that the cherry-picker can neither see the changeOwner, the reviewer, the cc nor the
+ // authorCommitter.
+ requestScopeOperations.setApiUser(cherryPicker.id());
+ assertThatAccountIsNotVisible(changeOwner, reviewer, cc, authorCommitter);
+
+ // Create the change with authorCommitter as the author and the committer.
+ requestScopeOperations.setApiUser(changeOwner.id());
+ PushOneCommit push = pushFactory.create(authorCommitter.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+
+ // Check that authorCommitter was set as the author and committer.
+ ChangeInfo changeInfo = gApi.changes().id(r.getChangeId()).get();
+ CommitInfo commit = changeInfo.revisions.get(changeInfo.currentRevision).commit;
+ assertThat(commit.author.email).isEqualTo(authorCommitter.email());
+ assertThat(commit.committer.email).isEqualTo(authorCommitter.email());
+
+ // Pushing a commit with a forged author/committer adds the author/committer as a CC.
+ assertCcs(r.getChangeId(), authorCommitter);
+
+ // Remove the author/committer as a CC because because otherwise there are two signals for CCing
+ // authorCommitter on the cherry-pick change: once because they are author and committer and
+ // once because they are a CC. For authorCommitter we only want to test the first signal here
+ // (the second signal is covered by adding an explicit CC below).
+ gApi.changes().id(r.getChangeId()).reviewer(authorCommitter.email()).remove();
+ assertNoCcs(r.getChangeId());
+
+ // Add reviewer and cc.
+ ReviewInput reviewerInput = ReviewInput.approve();
+ reviewerInput.reviewer(reviewer.email());
+ reviewerInput.cc(cc.email());
+ gApi.changes().id(r.getChangeId()).current().review(reviewerInput);
+
+ // Approve and submit the change.
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ // Cherry-pick the change.
+ requestScopeOperations.setApiUser(cherryPicker.id());
+ CherryPickInput cherryPickInput = new CherryPickInput();
+ cherryPickInput.message = "Cherry-pick to stable branch";
+ cherryPickInput.destination = "stable";
+ cherryPickInput.keepReviewers = true;
+ String cherryPickChangeId =
+ gApi.changes().id(r.getChangeId()).current().cherryPick(cherryPickInput).get().id;
+
+ // Cherry-pick doesn't check the visibility of explicit reviewers/CCs. Since the cherry-picker
+ // can see the cherry-picked change, they can also see its reviewers/CCs. This means preserving
+ // them on the cherry-pick change doesn't expose their account existence and it's OK to keep
+ // them even if their accounts are not visible to the cherry-picker.
+ // In contrast to this for implicit CCs that are added for the author/committer the account
+ // visibility is checked, but if their accounts are not visible the CC is silently dropped (so
+ // that the cherry-pick request can still succeed). Since in this case authorCommitter is not
+ // visible, we expect that CCing them is being dropped and hence authorCommitter is not returned
+ // as a CC here. The reason that the visibility for author/committer must be checked is that
+ // author/committer may not match a Gerrit account (if they are forged). This means by seeing
+ // the author/committer on the cherry-picked change, it's not possible to deduce that these
+ // Gerrit accounts exists, but if they would be added as a CC on the cherry-pick change even if
+ // they are not visible the account existence would be exposed.
+ assertReviewers(cherryPickChangeId, changeOwner, reviewer);
+ assertCcs(cherryPickChangeId, cc);
+ }
+
+ @Test
public void cherryPickToMergedChangeRevision() throws Exception {
createBranch(BranchNameKey.create(project, "foo"));
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index a16cdb6..1363ce7 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -588,6 +588,22 @@
}
@Test
+ public void commentWithRangeAndLine_lineIsIgnored() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ withFixRobotCommentInput.line = 1;
+ withFixRobotCommentInput.range = createRange(2, 0, 3, 1);
+ withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotComments = getRobotComments();
+ assertThat(robotComments.get(0).line).isEqualTo(3);
+ }
+
+ @Test
public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap() {
FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
fixReplacementInfo1.path = FILE_NAME;
@@ -1450,7 +1466,7 @@
}
@Test
- public void PreviewStoredFixForNonExistingFile() throws Exception {
+ public void previewStoredFixForNonExistingFile() throws Exception {
FixReplacementInfo replacement = new FixReplacementInfo();
replacement.path = "a_non_existent_file.txt";
replacement.range = createRange(1, 0, 2, 0);
@@ -1471,7 +1487,7 @@
}
@Test
- public void PreviewStoredFix() throws Exception {
+ public void previewStoredFix() throws Exception {
FixReplacementInfo fixReplacementInfoFile1 = new FixReplacementInfo();
fixReplacementInfoFile1.path = FILE_NAME;
fixReplacementInfoFile1.replacement = "some replacement code";
@@ -1581,7 +1597,7 @@
}
@Test
- public void PreviewStoredFixAddNewLineAtEnd() throws Exception {
+ public void previewStoredFixAddNewLineAtEnd() throws Exception {
FixReplacementInfo replacement = new FixReplacementInfo();
replacement.path = FILE_NAME3;
replacement.range = createRange(2, 8, 2, 8);
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 9fae6c0..edfb577 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -94,6 +94,7 @@
private static final String FILE_NAME = "foo";
private static final String FILE_NAME2 = "foo2";
private static final String FILE_NAME3 = "foo3";
+ private static final int FILE_MODE = 100644;
private static final byte[] CONTENT_OLD = "bar".getBytes(UTF_8);
private static final byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
@@ -686,6 +687,16 @@
}
@Test
+ public void changeEditModifyFileModeRest() throws Exception {
+ createEmptyEditFor(changeId);
+ FileContentInput in = new FileContentInput();
+ in.binary_content = CONTENT_BINARY_ENCODED_NEW;
+ in.fileMode = FILE_MODE;
+ adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
+ ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW);
+ }
+
+ @Test
public void createAndUploadBinaryInChangeEditOneRequestRest() throws Exception {
FileContentInput in = new FileContentInput();
in.binary_content = CONTENT_BINARY_ENCODED_NEW;
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index cd1d911..08f65da 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -128,6 +128,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
@@ -1154,7 +1155,7 @@
.add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
.message(PushOneCommit.SUBJECT)
.create();
- // Push commit as "Admnistrator".
+ // Push commit as "Administrator".
pushHead(testRepo, "refs/for/master");
String changeId = GitUtil.getChangeId(testRepo, c).get();
@@ -1168,6 +1169,71 @@
}
@Test
+ public void pushForMasterWithNonExistingForgedAuthorAndCommitter() throws Exception {
+ // Create a commit with different forged author and committer.
+ RevCommit c =
+ commitBuilder()
+ .author(new PersonIdent("author", "author@example.com"))
+ .committer(new PersonIdent("committer", "committer@example.com"))
+ .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+ .message(PushOneCommit.SUBJECT)
+ .create();
+ // Push commit as "Administrator".
+ pushHead(testRepo, "refs/for/master");
+
+ String changeId = GitUtil.getChangeId(testRepo, c).get();
+ assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email());
+ assertThat(getReviewerEmails(changeId, ReviewerState.CC)).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void pushForMasterWithNonVisibleForgedAuthorAndCommitter() throws Exception {
+ // Define readable names for the users we use in this test.
+ TestAccount uploader = user; // cannot use admin since admin can see all users
+ TestAccount author = accountCreator.user2();
+ TestAccount committer =
+ accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
+
+ // Check that the uploader can neither see the author nor the committer.
+ requestScopeOperations.setApiUser(uploader.id());
+ assertThatAccountIsNotVisible(author, committer);
+
+ // Allow the uploader to forge author and committer.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.FORGE_AUTHOR).ref("refs/heads/master").group(REGISTERED_USERS))
+ .add(allow(Permission.FORGE_COMMITTER).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Clone the repo as uploader so that the push is done by the uplaoder.
+ TestRepository<InMemoryRepository> testRepo = cloneProject(project, uploader);
+
+ // Create a commit with different forged author and committer.
+ RevCommit c =
+ testRepo
+ .branch("HEAD")
+ .commit()
+ .insertChangeId()
+ .author(author.newIdent())
+ .committer(committer.newIdent())
+ .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+ .message(PushOneCommit.SUBJECT)
+ .create();
+
+ PushResult r = pushHead(testRepo, "refs/for/master");
+ RemoteRefUpdate refUpdate = r.getRemoteUpdate("refs/for/master");
+ assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
+
+ String changeId = GitUtil.getChangeId(testRepo, c).get();
+ assertThat(getOwnerEmail(changeId)).isEqualTo(uploader.email());
+
+ // author and committer have not been CCed because their accounts are not visible
+ assertThat(getReviewerEmails(changeId, ReviewerState.CC)).isEmpty();
+ }
+
+ @Test
public void pushNewPatchSetForMasterWithForgedAuthorAndCommitter() throws Exception {
TestAccount user2 = accountCreator.user2();
// First patch set has author and committer matching change owner.
@@ -1192,6 +1258,74 @@
.containsExactly(user.getNameEmail(), user2.getNameEmail());
}
+ @Test
+ public void pushNewPatchSetForMasterWithNonExistingForgedAuthorAndCommitter() throws Exception {
+ // First patch set has author and committer matching change owner.
+ PushOneCommit.Result r = pushTo("refs/for/master");
+
+ assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email());
+ assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)).isEmpty();
+
+ amendBuilder()
+ .author(new PersonIdent("author", "author@example.com"))
+ .committer(new PersonIdent("committer", "committer@example.com"))
+ .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2")
+ .create();
+ pushHead(testRepo, "refs/for/master");
+
+ assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email());
+ assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.CC)).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void pushNewPatchSetForMasterWithNonVisibleForgedAuthorAndCommitter() throws Exception {
+ // Define readable names for the users we use in this test.
+ TestAccount uploader = user; // cannot use admin since admin can see all users
+ TestAccount author = accountCreator.user2();
+ TestAccount committer =
+ accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
+
+ // Check that the uploader can neither see the author nor the committer.
+ requestScopeOperations.setApiUser(uploader.id());
+ assertThatAccountIsNotVisible(author, committer);
+
+ // Allow the uploader to forge author and committer.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.FORGE_AUTHOR).ref("refs/heads/master").group(REGISTERED_USERS))
+ .add(allow(Permission.FORGE_COMMITTER).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Clone the repo as uploader so that the push is done by the uplaoder.
+ TestRepository<InMemoryRepository> testRepo = cloneProject(project, uploader);
+
+ // First patch set has author and committer matching uploader.
+ PushOneCommit push = pushFactory.create(uploader.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+
+ assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(uploader.email());
+ assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)).isEmpty();
+
+ testRepo
+ .amendRef("HEAD")
+ .author(author.newIdent())
+ .committer(committer.newIdent())
+ .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2")
+ .create();
+
+ PushResult r2 = pushHead(testRepo, "refs/for/master");
+ RemoteRefUpdate refUpdate = r2.getRemoteUpdate("refs/for/master");
+ assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
+
+ assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(uploader.email());
+
+ // author and committer have not been CCed because their accounts are not visible
+ assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.CC)).isEmpty();
+ }
+
/**
* There was a bug that allowed a user with Forge Committer Identity access right to upload a
* commit and put *votes on behalf of another user* on it. This test checks that this is not
@@ -2544,6 +2678,23 @@
}
@Test
+ @GerritConfig(
+ name = "plugins.transitionalPushOptions",
+ values = {"gerrit~foo", "gerrit~bar"})
+ public void transitionalPushOptionsArePassedToCommitValidationListener() throws Exception {
+ TestValidator validator = new TestValidator();
+ try (Registration registration = extensionRegistry.newRegistration().add(validator)) {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
+ push.setPushOptions(ImmutableList.of("trace=123", "gerrit~foo", "gerrit~bar=456"));
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(validator.pushOptions())
+ .containsExactly("trace", "123", "gerrit~foo", "", "gerrit~bar", "456");
+ }
+ }
+
+ @Test
public void pluginPushOptionsHelp() throws Exception {
PluginPushOption fooOption = new TestPluginPushOption("foo", "some description");
PluginPushOption barOption = new TestPluginPushOption("bar", "other description");
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
new file mode 100644
index 0000000..7aadc08
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -0,0 +1,383 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.pgm;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.projects.SubmitRequirementApi;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement;
+import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement.Status;
+import com.google.gerrit.server.schema.UpdateUI;
+import com.google.inject.Inject;
+import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/** Test for {@link com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement}. */
+@Sandboxed
+public class MigrateLabelFunctionsToSubmitRequirementIT extends AbstractDaemonTest {
+
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void migrateBlockingLabel_maxWithBlock() throws Exception {
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_maxNoBlock() throws Exception {
+ createLabel("Foo", "MaxNoBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_anyWithBlock() throws Exception {
+ createLabel("Foo", "AnyWithBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "-label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_maxWithBlock_withIgnoreSelfApproval() throws Exception {
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ true);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX,user=non_uploader AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_maxNoBlock_withIgnoreSelfApproval() throws Exception {
+ createLabel("Foo", "MaxNoBlock", /* ignoreSelfApproval= */ true);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX,user=non_uploader",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateNonBlockingLabel_NoBlock() throws Exception {
+ createLabel("Foo", "NoBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "is:false",
+ /* submittabilityExpression= */ "is:true",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateNonBlockingLabel_NoOp() throws Exception {
+ createLabel("Foo", "NoBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "is:false",
+ /* submittabilityExpression= */ "is:true",
+ /* canOverride= */ true);
+
+ // The NoOp function is converted to NoBlock. Both are same.
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateNonBlockingLabel_PatchSetLock_doesNothing() throws Exception {
+ createLabel("Foo", "PatchSetLock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.NO_CHANGE);
+ // No submit requirement created for the patchset lock label function
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(0);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertNonExistentSr(/* srName = */ "Foo");
+ assertLabelFunction("Foo", "PatchSetLock");
+ }
+
+ @Test
+ public void migrationIsCommittedWithServerIdent() throws Exception {
+ RevCommit oldMetaCommit = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+
+ RevCommit newMetaCommit = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
+ assertThat(newMetaCommit).isNotEqualTo(oldMetaCommit);
+ assertThat(newMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ }
+
+ @Test
+ public void migrationIsIdempotent() throws Exception {
+ String oldRefsConfigId;
+ try (Repository repo = repoManager.openRepository(project)) {
+ oldRefsConfigId = repo.exactRef(RefNames.REFS_CONFIG).getObjectId().toString();
+ }
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ false);
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ // Running the migration causes REFS_CONFIG to change.
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+ try (Repository repo = repoManager.openRepository(project)) {
+ assertThat(oldRefsConfigId)
+ .isNotEqualTo(repo.exactRef(RefNames.REFS_CONFIG).getObjectId().toString());
+ oldRefsConfigId = repo.exactRef(RefNames.REFS_CONFIG).getObjectId().toString();
+ }
+
+ // No new SRs will be created. No conflicting submit requirements either since the migration
+ // detects that a previous run was made and skips the migration.
+ updateUI = runMigration(/* expectedResult= */ Status.PREVIOUSLY_MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(0);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+ // Running the migration a second time won't update REFS_CONFIG.
+ try (Repository repo = repoManager.openRepository(project)) {
+ assertThat(oldRefsConfigId)
+ .isEqualTo(repo.exactRef(RefNames.REFS_CONFIG).getObjectId().toString());
+ }
+ }
+
+ @Test
+ public void migrationDoesNotCreateANewSubmitRequirement_ifSRAlreadyExists_matchingWithMigration()
+ throws Exception {
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ false);
+ createSubmitRequirement("Foo", "label:Foo=MAX AND -label:Foo=MIN", /* canOverride= */ true);
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ // No new submit requirements are created.
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(0);
+ // No conflicting submit requirements from migration vs. what was previously configured.
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ // The existing SR was left as is.
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ }
+
+ @Test
+ public void
+ migrationDoesNotCreateANewSubmitRequirement_ifSRAlreadyExists_mismatchingWithMigration()
+ throws Exception {
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ false);
+ createSubmitRequirement("Foo", "project:" + project, /* canOverride= */ true);
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "project:" + project,
+ /* canOverride= */ true);
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ // One conflicting submit requirement between migration vs. what was previously configured.
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(1);
+
+ // The existing SR was left as is.
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ null,
+ /* submittabilityExpression= */ "project:" + project,
+ /* canOverride= */ true);
+ }
+
+ @Test
+ public void migrationResetsBlockingLabel_ifSRAlreadyExists() throws Exception {
+ createLabel("Foo", "MaxWithBlock", /* ignoreSelfApproval= */ false);
+ createSubmitRequirement("Foo", "owner:" + admin.email(), /* canOverride= */ true);
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(0);
+
+ // The label function was reset
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ private TestUpdateUI runMigration(Status expectedResult) throws Exception {
+ TestUpdateUI updateUi = new TestUpdateUI();
+ MigrateLabelFunctionsToSubmitRequirement executor =
+ new MigrateLabelFunctionsToSubmitRequirement(repoManager, serverIdent.get());
+ Status status = executor.executeMigration(project, updateUi);
+ assertThat(status).isEqualTo(expectedResult);
+ projectCache.evictAndReindex(project);
+ return updateUi;
+ }
+
+ private void createLabel(String labelName, String function, boolean ignoreSelfApproval)
+ throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = labelName;
+ input.function = function;
+ input.ignoreSelfApproval = ignoreSelfApproval;
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ gApi.projects().name(project.get()).label(labelName).create(input);
+ }
+
+ @CanIgnoreReturnValue
+ private SubmitRequirementApi createSubmitRequirement(
+ String name, String submitExpression, boolean canOverride) throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = name;
+ input.submittabilityExpression = submitExpression;
+ input.allowOverrideInChildProjects = canOverride;
+ return gApi.projects().name(project.get()).submitRequirement(name).create(input);
+ }
+
+ private void assertLabelFunction(String labelName, String function) throws Exception {
+ LabelDefinitionInfo info = gApi.projects().name(project.get()).label(labelName).get();
+ assertThat(info.function).isEqualTo(function);
+ }
+
+ private void assertNonExistentSr(String srName) {
+ ResourceNotFoundException foo =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).submitRequirement("Foo").get());
+ assertThat(foo.getMessage()).isEqualTo("Submit requirement '" + srName + "' does not exist");
+ }
+
+ private void assertExistentSr(
+ String srName,
+ String applicabilityExpression,
+ String submittabilityExpression,
+ boolean canOverride)
+ throws Exception {
+ SubmitRequirementInfo sr = gApi.projects().name(project.get()).submitRequirement(srName).get();
+ assertThat(sr.applicabilityExpression).isEqualTo(applicabilityExpression);
+ assertThat(sr.submittabilityExpression).isEqualTo(submittabilityExpression);
+ assertThat(sr.allowOverrideInChildProjects).isEqualTo(canOverride);
+ }
+
+ private static class TestUpdateUI implements UpdateUI {
+ int existingSrsMismatchingWithMigration = 0;
+ int newlyCreatedSrs = 0;
+
+ @Override
+ public void message(String message) {
+ if (message.startsWith("Warning")) {
+ existingSrsMismatchingWithMigration += 1;
+ } else if (message.startsWith("Project")) {
+ newlyCreatedSrs += 1;
+ }
+ }
+
+ @Override
+ public boolean yesno(boolean defaultValue, String message) {
+ return false;
+ }
+
+ @Override
+ public void waitForUser() {}
+
+ @Override
+ public String readString(String defaultValue, Set<String> allowedValues, String message) {
+ return null;
+ }
+
+ @Override
+ public boolean isBatch() {
+ return false;
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index dbebbf9..67c784b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -19,7 +19,10 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.entities.Permission.CREATE;
import static com.google.gerrit.entities.Permission.READ;
+import static com.google.gerrit.entities.RefNames.HEAD;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.common.testing.GitPersonSubject.assertThat;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -47,6 +50,7 @@
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -59,6 +63,7 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -74,6 +79,7 @@
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.gson.stream.JsonReader;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
@@ -85,6 +91,8 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.api.errors.PatchApplyException;
+import org.eclipse.jgit.api.errors.PatchFormatException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -94,6 +102,7 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.util.Base64;
import org.junit.Before;
import org.junit.Test;
@@ -923,6 +932,167 @@
}
@Test
+ public void createChangeWithBothMergeAndPatch_fails() throws Exception {
+ ChangeInput input = newMergeChangeInput("foo", "master", "");
+ input.patch = new ApplyPatchInput();
+ assertCreateFails(
+ input, BadRequestException.class, "Only one of `merge` and `patch` arguments can be set");
+ }
+
+ private static final String PATCH_FILE_NAME = "a_file.txt";
+ private static final String PATCH_NEW_FILE_CONTENT = "First added line\nSecond added line\n";
+ private static final String PATCH_INPUT =
+ "diff --git a/a_file.txt b/a_file.txt\n"
+ + "new file mode 100644\n"
+ + "index 0000000..f0eec86\n"
+ + "--- /dev/null\n"
+ + "+++ b/a_file.txt\n"
+ + "@@ -0,0 +1,2 @@\n"
+ + "+First added line\n"
+ + "+Second added line\n";
+ private static final String MODIFICATION_PATCH_INPUT =
+ "diff --git a/a_file.txt b/a_file.txt\n"
+ + "new file mode 100644\n"
+ + "--- a/a_file.txt\n"
+ + "+++ b/a_file.txt.txt\n"
+ + "@@ -1,2 +1 @@\n"
+ + "-First original line\n"
+ + "-Second original line\n"
+ + "+Modified line\n";
+
+ @Test
+ public void createPatchApplyingChange_success() throws Exception {
+ createBranch(BranchNameKey.create(project, "other"));
+ ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
+
+ ChangeInfo info = assertCreateSucceeds(input);
+
+ DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+ assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ }
+
+ @Test
+ public void createPatchApplyingChange_fromGerritPatch_success() throws Exception {
+ String head = getHead(repo(), HEAD).name();
+ createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+ PushOneCommit.Result baseCommit =
+ createChange("Add file", PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ baseCommit.assertOkStatus();
+ BinaryResult originalPatch = gApi.changes().id(baseCommit.getChangeId()).current().patch();
+ createBranchWithRevision(BranchNameKey.create(project, "other"), head);
+ ChangeInput input = newPatchApplyingChangeInput("other", originalPatch.asString());
+
+ ChangeInfo info = assertCreateSucceeds(input);
+
+ DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+ assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ }
+
+ @Test
+ public void createPatchApplyingChange_fromGerritPatchUsingRest_success() throws Exception {
+ String head = getHead(repo(), HEAD).name();
+ createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+ PushOneCommit.Result baseCommit =
+ createChange("Add file", PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ baseCommit.assertOkStatus();
+ createBranchWithRevision(BranchNameKey.create(project, "other"), head);
+ RestResponse patchResp =
+ userRestSession.get("/changes/" + baseCommit.getChangeId() + "/revisions/current/patch");
+ patchResp.assertOK();
+ String originalPatch = new String(Base64.decode(patchResp.getEntityContent()), UTF_8);
+ ChangeInput input = newPatchApplyingChangeInput("other", originalPatch);
+
+ ChangeInfo info = assertCreateSucceedsUsingRest(input);
+
+ DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+ assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ }
+
+ @Test
+ public void createPatchApplyingChange_withParentChange_success() throws Exception {
+ Result change = createChange();
+ ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
+ input.baseChange = change.getChangeId();
+
+ ChangeInfo info = assertCreateSucceeds(input);
+
+ assertThat(gApi.changes().id(info.id).current().commit(false).parents.get(0).commit)
+ .isEqualTo(change.getCommit().getId().name());
+ DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+ assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ }
+
+ @Test
+ public void createPatchApplyingChange_withParentCommit_success() throws Exception {
+ createBranch(BranchNameKey.create(project, "other"));
+ Result baseChange = createChange("refs/heads/other");
+ PushOneCommit.Result ignoredCommit = createChange();
+ ignoredCommit.assertOkStatus();
+ ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
+ input.baseCommit = baseChange.getCommit().getId().name();
+
+ ChangeInfo info = assertCreateSucceeds(input);
+
+ assertThat(gApi.changes().id(info.id).current().commit(false).parents.get(0).commit)
+ .isEqualTo(input.baseCommit);
+ DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+ assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ }
+
+ @Test
+ public void createPatchApplyingChange_withEmptyTip_fails() throws Exception {
+ ChangeInput input = newPatchApplyingChangeInput("foo", "patch");
+ input.newBranch = true;
+ assertCreateFails(
+ input, BadRequestException.class, "Cannot apply patch on top of an empty tree");
+ }
+
+ @Test
+ public void createPatchApplyingChange_fromBadPatch_fails() throws Exception {
+ final String invalidPatch = "@@ -2,2 +2,3 @@ a\n" + " b\n" + "+c\n" + " d";
+ createBranch(BranchNameKey.create(project, "other"));
+ ChangeInput input = newPatchApplyingChangeInput("other", invalidPatch);
+ assertCreateFailsWithCause(
+ input, BadRequestException.class, PatchFormatException.class, "Format error");
+ }
+
+ @Test
+ public void createPatchApplyingChange_withAuthorOverride_success() throws Exception {
+ createBranch(BranchNameKey.create(project, "other"));
+ ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
+ input.author = new AccountInput();
+ input.author.email = "gerritlessjane@invalid";
+ // This is an email address that doesn't exist as account on the Gerrit server.
+ input.author.name = "Gerritless Jane";
+ ChangeInfo info = assertCreateSucceeds(input);
+
+ RevisionApi rApi = gApi.changes().id(info.id).current();
+ GitPerson author = rApi.commit(false).author;
+ assertThat(author).email().isEqualTo(input.author.email);
+ assertThat(author).name().isEqualTo(input.author.name);
+ GitPerson committer = rApi.commit(false).committer;
+ assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
+ }
+
+ @Test
+ public void createPatchApplyingChange_withInfeasiblePatch_fails() throws Exception {
+ createBranch(BranchNameKey.create(project, "other"));
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Adding unexpected base content, which will cause the patch to fail",
+ PATCH_FILE_NAME,
+ "unexpected base content");
+ Result conflictingChange = push.to("refs/heads/other");
+ conflictingChange.assertOkStatus();
+ ChangeInput input = newPatchApplyingChangeInput("other", MODIFICATION_PATCH_INPUT);
+
+ assertCreateFailsWithCause(
+ input, RestApiException.class, PatchApplyException.class, "Cannot apply: HunkHeader");
+ }
+
+ @Test
@UseSystemTime
public void sha1sOfTwoNewChangesDiffer() throws Exception {
ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
@@ -1084,17 +1254,38 @@
private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
ChangeInfo out = gApi.changes().create(in).get();
+ validateCreateSucceeds(in, out);
+ return out;
+ }
+
+ private ChangeInfo assertCreateSucceedsUsingRest(ChangeInput in) throws Exception {
+ RestResponse resp = adminRestSession.post("/changes/", in);
+ resp.assertCreated();
+ ChangeInfo res = readContentFromJson(resp, ChangeInfo.class);
+ // The original result doesn't contain any revision data.
+ ChangeInfo out = gApi.changes().id(res.changeId).get(ALL_REVISIONS, CURRENT_COMMIT);
+ validateCreateSucceeds(in, out);
+ return out;
+ }
+
+ private static <T> T readContentFromJson(RestResponse r, Class<T> clazz) throws Exception {
+ try (JsonReader jsonReader = new JsonReader(r.getReader())) {
+ return newGson().fromJson(jsonReader, clazz);
+ }
+ }
+
+ private void validateCreateSucceeds(ChangeInput in, ChangeInfo out) throws Exception {
assertThat(out.project).isEqualTo(in.project);
assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(out.subject).isEqualTo(Splitter.on("\n").splitToList(in.subject).get(0));
assertThat(out.topic).isEqualTo(in.topic);
assertThat(out.status).isEqualTo(in.status);
- if (in.isPrivate) {
+ if (Boolean.TRUE.equals(in.isPrivate)) {
assertThat(out.isPrivate).isTrue();
} else {
assertThat(out.isPrivate).isNull();
}
- if (in.workInProgress) {
+ if (Boolean.TRUE.equals(in.workInProgress)) {
assertThat(out.workInProgress).isTrue();
} else {
assertThat(out.workInProgress).isNull();
@@ -1103,7 +1294,6 @@
assertThat(out.submitted).isNull();
assertThat(out.containsGitConflicts).isNull();
assertThat(in.status).isEqualTo(ChangeStatus.NEW);
- return out;
}
private ChangeInfo assertCreateSucceedsWithConflicts(ChangeInput in) throws Exception {
@@ -1132,6 +1322,17 @@
assertThat(thrown).hasMessageThat().contains(errSubstring);
}
+ private void assertCreateFailsWithCause(
+ ChangeInput in,
+ Class<? extends RestApiException> errType,
+ Class<? extends Exception> causeType,
+ String causeSubstring)
+ throws Exception {
+ Throwable thrown = assertThrows(errType, () -> gApi.changes().create(in));
+ assertThat(thrown).hasCauseThat().isInstanceOf(causeType);
+ assertThat(thrown).hasCauseThat().hasMessageThat().contains(causeSubstring);
+ }
+
// TODO(davido): Expose setting of account preferences in the API
private void setSignedOffByFooter(boolean value) throws Exception {
RestResponse r = adminRestSession.get("/accounts/" + admin.email() + "/preferences");
@@ -1174,6 +1375,19 @@
return in;
}
+ private ChangeInput newPatchApplyingChangeInput(String targetBranch, String patch) {
+ // create a change applying the given patch on the target branch in gerrit
+ ChangeInput in = new ChangeInput();
+ in.project = project.get();
+ in.branch = targetBranch;
+ in.subject = "apply patch to " + targetBranch;
+ in.status = ChangeStatus.NEW;
+ ApplyPatchInput patchInput = new ApplyPatchInput();
+ patchInput.patch = patch;
+ in.patch = patchInput;
+ return in;
+ }
+
/**
* Create an empty commit in master, two new branches with one commit each.
*
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index 6d2c6dfa..a9e3cf6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -18,6 +18,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
import com.google.gson.reflect.TypeToken;
import java.util.List;
@@ -41,6 +42,7 @@
userRestSession.get("/config/server/tasks/" + getLogFileCompressorTaskId()).assertNotFound();
}
+ @Nullable
private String getLogFileCompressorTaskId() throws Exception {
RestResponse r = adminRestSession.get("/config/server/tasks/");
List<TaskInfo> result =
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 6d980c7..2123ac2 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -185,6 +185,39 @@
}
@Test
+ public void commentWithRangeAndLine_lineIsIgnored() throws Exception {
+ String file = "file";
+ String contents = "contents";
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "first subject", file, contents);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ ReviewInput input = new ReviewInput();
+ CommentInput comment = CommentsUtil.newComment(file, Side.REVISION, 1, "comment 1", false);
+ int rangeEndLine = 3;
+ comment.range = createRange(1, 1, rangeEndLine, 3);
+ input.comments = new HashMap<>();
+ input.comments.put(comment.path, Lists.newArrayList(comment));
+ revision(r).review(input);
+ Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+ assertThat(result).isNotEmpty();
+ CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+ assertThat(actual.line).isEqualTo(rangeEndLine);
+ input = new ReviewInput();
+ comment = CommentsUtil.newComment(file, Side.REVISION, 1, "comment 1 reply", false);
+ comment.range = createRange(1, 1, rangeEndLine, 3);
+ // Post another comment in reply, and the line is still fixed to the range.endLine
+ comment.inReplyTo = actual.id;
+ input.comments = new HashMap<>();
+ input.comments.put(comment.path, Lists.newArrayList(comment));
+ revision(r).review(input);
+ result = getPublishedComments(changeId, revId);
+ assertThat(result.get(comment.path)).hasSize(2);
+ assertThat(result.get(comment.path).stream().allMatch(c -> c.line == rangeEndLine)).isTrue();
+ }
+
+ @Test
public void patchsetLevelCommentCanBeAddedAndRetrieved() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
@@ -2073,6 +2106,16 @@
return range;
}
+ private static Comment.Range createRange(
+ int startLine, int startCharacter, int endLine, int endCharacter) {
+ Comment.Range range = new Comment.Range();
+ range.startLine = startLine;
+ range.startCharacter = startCharacter;
+ range.endLine = endLine;
+ range.endCharacter = endCharacter;
+ return range;
+ }
+
private static Function<CommentInfo, CommentInput> infoToInput(String path) {
return info -> {
CommentInput commentInput = new CommentInput();
diff --git a/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java b/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
new file mode 100644
index 0000000..fdfef87
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
@@ -0,0 +1,285 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.git.WorkQueue.TaskListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TaskListenerIT extends AbstractDaemonTest {
+ /**
+ * Use a LatchedMethod in a method to allow another thread to await the method's call. Once
+ * called, the Latch.call() method will block until another thread calls its LatchedMethods's
+ * complete() method.
+ */
+ private static class LatchedMethod {
+ private static final int AWAIT_TIMEOUT = 20;
+ private static final TimeUnit AWAIT_TIMEUNIT = TimeUnit.MILLISECONDS;
+
+ /** API class meant be used by the class whose method is being latched */
+ private class Latch {
+ /** Ensure that the latched method calls this on entry */
+ public void call() {
+ called.countDown();
+ await(complete);
+ }
+ }
+
+ public Latch latch = new Latch();
+
+ private final CountDownLatch called = new CountDownLatch(1);
+ private final CountDownLatch complete = new CountDownLatch(1);
+
+ /** Assert that the Latch's call() method has not yet been called */
+ public void assertUncalled() {
+ assertThat(called.getCount()).isEqualTo(1);
+ }
+
+ /**
+ * Assert that a timeout does not occur while awaiting Latch's call() method to be called. Fails
+ * if the waiting time elapses before Latch's call() method is called, otherwise passes.
+ */
+ public void assertAwait() {
+ assertThat(await(called)).isEqualTo(true);
+ }
+
+ /** Unblock the Latch's call() method so that it can complete */
+ public void complete() {
+ complete.countDown();
+ }
+
+ @CanIgnoreReturnValue
+ private static boolean await(CountDownLatch latch) {
+ try {
+ return latch.await(AWAIT_TIMEOUT, AWAIT_TIMEUNIT);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+ }
+
+ private static class LatchedRunnable implements Runnable {
+ public LatchedMethod run = new LatchedMethod();
+
+ @Override
+ public void run() {
+ run.latch.call();
+ }
+ }
+
+ private static class ForwardingListener implements TaskListener {
+ public volatile TaskListener delegate;
+ public volatile Task<?> task;
+
+ public void resetDelegate(TaskListener listener) {
+ delegate = listener;
+ task = null;
+ }
+
+ @Override
+ public void onStart(Task<?> task) {
+ if (delegate != null) {
+ if (this.task == null || this.task == task) {
+ this.task = task;
+ delegate.onStart(task);
+ }
+ }
+ }
+
+ @Override
+ public void onStop(Task<?> task) {
+ if (delegate != null) {
+ if (this.task == task) {
+ delegate.onStop(task);
+ }
+ }
+ }
+ }
+
+ private static class LatchedListener implements TaskListener {
+ public LatchedMethod onStart = new LatchedMethod();
+ public LatchedMethod onStop = new LatchedMethod();
+
+ @Override
+ public void onStart(Task<?> task) {
+ onStart.latch.call();
+ }
+
+ @Override
+ public void onStop(Task<?> task) {
+ onStop.latch.call();
+ }
+ }
+
+ private static ForwardingListener forwarder;
+
+ @Inject private WorkQueue workQueue;
+ private ScheduledExecutorService executor;
+
+ private final LatchedListener listener = new LatchedListener();
+ private final LatchedRunnable runnable = new LatchedRunnable();
+
+ @Override
+ public Module createModule() {
+ return new AbstractModule() {
+ @Override
+ public void configure() {
+ // Forwarder.delegate is empty on start to protect test listener from non test tasks
+ // (such as the "Log File Compressor") interference
+ forwarder = new ForwardingListener(); // Only gets bound once for all tests
+ bind(TaskListener.class).annotatedWith(Exports.named("listener")).toInstance(forwarder);
+ }
+ };
+ }
+
+ @Before
+ public void setupExecutorAndForwarder() throws InterruptedException {
+ executor = workQueue.createQueue(1, "TaskListeners");
+
+ // "Log File Compressor"s are likely running and will interfere with tests
+ while (0 != workQueue.getTasks().size()) {
+ for (Task<?> t : workQueue.getTasks()) {
+ @SuppressWarnings("unused")
+ boolean unused = t.cancel(true);
+ }
+ TimeUnit.MILLISECONDS.sleep(1);
+ }
+
+ forwarder.resetDelegate(listener);
+
+ assertQueueSize(0);
+ assertThat(forwarder.task).isEqualTo(null);
+ listener.onStart.assertUncalled();
+ runnable.run.assertUncalled();
+ listener.onStop.assertUncalled();
+ }
+
+ @Test
+ public void onStartThenRunThenOnStopAreCalled() throws Exception {
+ int size = assertQueueBlockedOnExecution(runnable);
+
+ // onStartThenRunThenOnStopAreCalled -> onStart...Called
+ listener.onStart.assertAwait();
+ assertQueueSize(size);
+ runnable.run.assertUncalled();
+ listener.onStop.assertUncalled();
+
+ listener.onStart.complete();
+ // onStartThenRunThenOnStopAreCalled -> ...ThenRun...Called
+ runnable.run.assertAwait();
+ listener.onStop.assertUncalled();
+
+ runnable.run.complete();
+ // onStartThenRunThenOnStopAreCalled -> ...ThenOnStop...Called
+ listener.onStop.assertAwait();
+ assertQueueSize(size);
+
+ listener.onStop.complete();
+ assertAwaitQueueSize(--size);
+ }
+
+ @Test
+ public void firstBlocksSecond() throws Exception {
+ int size = assertQueueBlockedOnExecution(runnable);
+
+ // firstBlocksSecond -> first...
+ listener.onStart.assertAwait();
+ assertQueueSize(size);
+
+ LatchedRunnable runnable2 = new LatchedRunnable();
+ size = assertQueueBlockedOnExecution(runnable2);
+
+ // firstBlocksSecond -> ...BlocksSecond
+ runnable2.run.assertUncalled();
+ assertQueueSize(size); // waiting on first
+
+ listener.onStart.complete();
+ runnable.run.assertAwait();
+ assertQueueSize(size); // waiting on first
+ runnable2.run.assertUncalled();
+
+ runnable.run.complete();
+ listener.onStop.assertAwait();
+ assertQueueSize(size); // waiting on first
+ runnable2.run.assertUncalled();
+
+ listener.onStop.complete();
+ runnable2.run.assertAwait();
+ assertQueueSize(--size);
+
+ runnable2.run.complete();
+ assertAwaitQueueSize(--size);
+ }
+
+ @Test
+ public void states() throws Exception {
+ executor.execute(runnable);
+ listener.onStart.assertAwait();
+ assertStateIs(Task.State.STARTING);
+
+ listener.onStart.complete();
+ runnable.run.assertAwait();
+ assertStateIs(Task.State.RUNNING);
+
+ runnable.run.complete();
+ listener.onStop.assertAwait();
+ assertStateIs(Task.State.STOPPING);
+
+ listener.onStop.complete();
+ assertAwaitQueueIsEmpty();
+ assertStateIs(Task.State.DONE);
+ }
+
+ private void assertStateIs(Task.State state) {
+ assertThat(forwarder.task.getState()).isEqualTo(state);
+ }
+
+ private int assertQueueBlockedOnExecution(Runnable runnable) {
+ int expectedSize = workQueue.getTasks().size() + 1;
+ executor.execute(runnable);
+ assertQueueSize(expectedSize);
+ return expectedSize;
+ }
+
+ private void assertQueueSize(int size) {
+ assertThat(workQueue.getTasks().size()).isEqualTo(size);
+ }
+
+ private void assertAwaitQueueIsEmpty() throws InterruptedException {
+ assertAwaitQueueSize(0);
+ }
+
+ /** Fails if the waiting time elapses before the count is reached, otherwise passes */
+ private void assertAwaitQueueSize(int size) throws InterruptedException {
+ long i = 0;
+ do {
+ TimeUnit.NANOSECONDS.sleep(10);
+ assertThat(i++).isLessThan(100);
+ } while (size != workQueue.getTasks().size());
+ }
+}
diff --git a/javatests/com/google/gerrit/extensions/BUILD b/javatests/com/google/gerrit/extensions/BUILD
index 2202a11..1bb39c8 100644
--- a/javatests/com/google/gerrit/extensions/BUILD
+++ b/javatests/com/google/gerrit/extensions/BUILD
@@ -5,6 +5,7 @@
size = "small",
srcs = glob(["**/*.java"]),
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/extensions/common/testing:common-test-util",
"//lib:guava",
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 024e35e..7ed236a 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.ReviewerState;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
@@ -344,6 +345,7 @@
assertThat(diff.removed().reviewers).isNull();
}
+ @Nullable
private static Object buildObjectWithFullFields(Class<?> c) throws Exception {
if (c == null) {
return null;
@@ -365,6 +367,7 @@
return toPopulate;
}
+ @Nullable
private static Class<?> getParameterizedType(Field field) {
if (!Collection.class.isAssignableFrom(field.getType())) {
return null;
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index 121cbc4..a69d60f 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -21,6 +21,7 @@
"//lib/bouncycastle:bcprov",
"//lib/guice",
"//lib/guice:guice-servlet",
+ "//lib/jsoup",
"//lib/mockito",
"//lib/truth",
"//lib/truth:truth-java8-extension",
diff --git a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
index 71695f3..04f9827 100644
--- a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -45,6 +45,7 @@
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
+import java.util.Collection;
import java.util.Optional;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -72,8 +73,6 @@
@Mock private AccountCache accountCache;
- @Mock private AccountState accountState;
-
@Mock private AccountManager accountManager;
@Mock private AuthConfig authConfig;
@@ -105,12 +104,8 @@
authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
pwdVerifier = new PasswordVerifier(extIdKeyFactory, authConfig);
- Account account = Account.builder(Account.id(1000000), Instant.now()).build();
authSuccessful =
new AuthResult(AUTH_ACCOUNT_ID, extIdKeyFactory.create("username", AUTH_USER), false);
- doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
- doReturn(Optional.of(accountState)).when(accountCache).get(AUTH_ACCOUNT_ID);
- doReturn(account).when(accountState).account();
doReturn(authSuccessful).when(accountManager).authenticate(any());
doReturn(new WebSessionManager.Key(AUTH_COOKIE_VALUE)).when(webSessionManager).createKey(any());
@@ -123,6 +118,7 @@
@Test
public void shouldAllowAnonymousRequest() throws Exception {
+ initAccount();
initMockedWebSession();
res.setStatus(HttpServletResponse.SC_OK);
@@ -143,6 +139,7 @@
@Test
public void shouldRequestAuthenticationForBasicAuthRequest() throws Exception {
+ initAccount();
initMockedWebSession();
req.addHeader("Authorization", "Basic " + AUTH_USER_B64);
res.setStatus(HttpServletResponse.SC_OK);
@@ -165,6 +162,7 @@
@Test
public void shouldAuthenticateSucessfullyAgainstRealmAndReturnCookie() throws Exception {
+ initAccount();
initWebSessionWithoutCookie();
requestBasicAuth(req);
res.setStatus(HttpServletResponse.SC_OK);
@@ -191,9 +189,10 @@
@Test
public void shouldValidateUserPasswordAndNotReturnCookie() throws Exception {
+ ExternalId extId = createUsernamePasswordExternalId();
+ initAccount(ImmutableSet.of(extId));
initWebSessionWithoutCookie();
requestBasicAuth(req);
- initMockedUsernamePasswordExternalId();
doReturn(GitBasicAuthPolicy.HTTP).when(authConfig).getGitBasicAuthPolicy();
res.setStatus(HttpServletResponse.SC_OK);
@@ -217,6 +216,7 @@
@Test
public void shouldNotReauthenticateForGitPostRequest() throws Exception {
+ initAccount();
req.setPathInfo("/a/project.git/git-upload-pack");
req.setMethod("POST");
req.addHeader("Content-Type", "application/x-git-upload-pack-request");
@@ -229,6 +229,7 @@
@Test
public void shouldReauthenticateForRegularRequestEvenIfAlreadySignedIn() throws Exception {
+ initAccount();
doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
doFilterForRequestWhenAlreadySignedIn();
@@ -239,6 +240,7 @@
@Test
public void shouldReauthenticateEvenIfHasExistingCookie() throws Exception {
+ initAccount();
initWebSessionWithCookie("GerritAccount=" + AUTH_COOKIE_VALUE);
res.setStatus(HttpServletResponse.SC_OK);
requestBasicAuth(req);
@@ -262,6 +264,7 @@
@Test
public void shouldFailedAuthenticationAgainstRealm() throws Exception {
+ initAccount();
initMockedWebSession();
requestBasicAuth(req);
@@ -285,6 +288,17 @@
assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
+ private void initAccount() throws Exception {
+ initAccount(ImmutableSet.of());
+ }
+
+ private void initAccount(Collection<ExternalId> extIds) throws Exception {
+ Account account = Account.builder(Account.id(1000000), Instant.now()).build();
+ AccountState accountState = AccountState.forAccount(account, extIds);
+ doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
+ doReturn(Optional.of(accountState)).when(accountCache).get(AUTH_ACCOUNT_ID);
+ }
+
private void doFilterForRequestWhenAlreadySignedIn()
throws IOException, ServletException, AccountException {
initMockedWebSession();
@@ -322,14 +336,12 @@
doReturn(webSession).when(webSessionItem).get();
}
- private void initMockedUsernamePasswordExternalId() {
- ExternalId extId =
- extIdFactory.createWithPassword(
- extIdKeyFactory.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
- AUTH_ACCOUNT_ID,
- null,
- AUTH_PASSWORD);
- doReturn(ImmutableSet.builder().add(extId).build()).when(accountState).externalIds();
+ private ExternalId createUsernamePasswordExternalId() {
+ return extIdFactory.createWithPassword(
+ extIdKeyFactory.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
+ AUTH_ACCOUNT_ID,
+ null,
+ AUTH_PASSWORD);
}
private void requestBasicAuth(FakeHttpServletRequest fakeReq) {
diff --git a/javatests/com/google/gerrit/httpd/raw/DocServletTest.java b/javatests/com/google/gerrit/httpd/raw/DocServletTest.java
new file mode 100644
index 0000000..94e1d21
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/raw/DocServletTest.java
@@ -0,0 +1,153 @@
+package com.google.gerrit.httpd.raw;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION;
+import static org.mockito.Mockito.when;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(JUnit4.class)
+public class DocServletTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock private ExperimentFeatures experimentFeatures;
+ private FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+ private DocServlet docServlet;
+
+ @Before
+ public void setUp() throws Exception {
+ when(experimentFeatures.isFeatureEnabled(GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION))
+ .thenReturn(true);
+
+ docServlet =
+ new DocServlet(
+ CacheBuilder.newBuilder().maximumSize(1).build(), false, experimentFeatures) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected Path getResourcePath(String pathInfo) throws IOException {
+ return fs.getPath("/" + CharMatcher.is('/').trimLeadingFrom(pathInfo));
+ }
+ };
+
+ Files.createDirectories(fs.getPath(DOC_PATH).getParent());
+ Files.write(fs.getPath(DOC_PATH), HTML_RESPONSE.getBytes(StandardCharsets.UTF_8));
+ Files.write(
+ fs.getPath(DOC_PATH_NO_SCRIPT), HTML_RESPONSE_NO_SCRIPT.getBytes(StandardCharsets.UTF_8));
+ Files.write(fs.getPath(NON_HTML_FILE_PATH), NON_HTML_FILE);
+ }
+
+ @Test
+ public void noNonce_unchangedResponse() throws Exception {
+ FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(DOC_PATH);
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ docServlet.doGet(request, response);
+
+ assertThat(response.getActualBody()).isEqualTo(HTML_RESPONSE.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void experimentDisabled_unchangedResponse() throws Exception {
+ when(experimentFeatures.isFeatureEnabled(GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION))
+ .thenReturn(false);
+ FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(DOC_PATH);
+ request.setAttribute("nonce", NONCE);
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ docServlet.doGet(request, response);
+
+ assertThat(response.getActualBody()).isEqualTo(HTML_RESPONSE.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void nonHtmlResponse_unchangedResponse() throws Exception {
+ FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(NON_HTML_FILE_PATH);
+ request.setAttribute("nonce", NONCE);
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ docServlet.doGet(request, response);
+
+ assertThat(response.getActualBody()).isEqualTo(NON_HTML_FILE);
+ }
+
+ @Test
+ public void responseWithoutScripts_equivalentResponse() throws Exception {
+ FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(DOC_PATH_NO_SCRIPT);
+ request.setAttribute("nonce", NONCE);
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ docServlet.doGet(request, response);
+
+ // Normally file is not guaranteed to not get reformatted, but in the simple example like we use
+ // here we can check byte-wise equality.
+ assertThat(response.getActualBody())
+ .isEqualTo(HTML_RESPONSE_NO_SCRIPT.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void htmlResponse_nonceAttached() throws Exception {
+ FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(DOC_PATH);
+ request.setAttribute("nonce", NONCE);
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ docServlet.doGet(request, response);
+
+ Document doc = Jsoup.parse(response.getActualBodyString());
+ for (Element el : doc.getElementsByTag("script")) {
+ assertThat(el.attributes().get("nonce")).isEqualTo(NONCE);
+ }
+ }
+
+ @Test
+ public void htmlResponse_noCacheHeaderSet() throws Exception {
+ FakeHttpServletRequest request = new FakeHttpServletRequest().setPathInfo(DOC_PATH);
+ request.setAttribute("nonce", NONCE);
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ docServlet.doGet(request, response);
+
+ assertThat(response.getHeader("Cache-Control"))
+ .isEqualTo("no-cache, no-store, max-age=0, must-revalidate");
+ }
+
+ private static final String NONCE = "1234abcde";
+ private static final String HTML_RESPONSE =
+ "<!DOCTYPE html>"
+ + "<html lang=\"en\">"
+ + "<head>"
+ + " <title>Gerrit Code Review - Searching Changes</title>"
+ + " <link rel=\"stylesheet\" href=\"./asciidoctor.css\">"
+ + " <script src=\"./prettify.min.js\"></script>"
+ + " <script>document.addEventListener('DOMContentLoaded', prettyPrint)</script>"
+ + "</head><body></body></html>";
+ private static final String DOC_PATH = "/Documentation/page1.html";
+ private static final String HTML_RESPONSE_NO_SCRIPT =
+ "<html><head></head><body><div>Hello</div></body></html>";
+ private static final String DOC_PATH_NO_SCRIPT = "/Documentation/page_no_script.html";
+ private static final byte[] NON_HTML_FILE = "<script></script>".getBytes(StandardCharsets.UTF_8);
+ private static final String NON_HTML_FILE_PATH = "/foo";
+}
diff --git a/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java b/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
index c2caff8..aa2605e 100644
--- a/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
+++ b/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.index.SchemaUtil.schema;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.index.SchemaFieldDefs.Getter;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
@@ -36,10 +37,21 @@
public void valid() {
IndexUpgradeValidator.assertValid(schema(1, ChangeField.ID), schema(2, ChangeField.ID));
IndexUpgradeValidator.assertValid(
- schema(1, ChangeField.ID), schema(2, ChangeField.ID, ChangeField.OWNER));
+ schema(1, ChangeField.ID),
+ schema(
+ 2,
+ ImmutableList.<FieldDef<ChangeData, ?>>of(ChangeField.ID),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.OWNER_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.OWNER_SPEC)));
IndexUpgradeValidator.assertValid(
schema(1, ChangeField.ID),
- schema(2, ChangeField.ID, ChangeField.OWNER, ChangeField.COMMITTER));
+ schema(
+ 2,
+ ImmutableList.<FieldDef<ChangeData, ?>>of(ChangeField.ID),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(
+ ChangeField.OWNER_FIELD, ChangeField.COMMITTER_PARTS_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.OWNER_SPEC, ChangeField.COMMITTER_PARTS_SPEC)));
}
@Test
@@ -49,7 +61,13 @@
AssertionError.class,
() ->
IndexUpgradeValidator.assertValid(
- schema(1, ChangeField.ID), schema(2, ChangeField.OWNER)));
+ schema(1, ChangeField.ID),
+ schema(
+ 2,
+ ImmutableList.<FieldDef<ChangeData, ?>>of(),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.OWNER_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.OWNER_SPEC))));
assertThat(e)
.hasMessageThat()
.contains("Schema upgrade to version 2 may either add or remove fields, but not both");
diff --git a/javatests/com/google/gerrit/index/query/AndSourceTest.java b/javatests/com/google/gerrit/index/query/AndSourceTest.java
index 8b95bff..068ae8c 100644
--- a/javatests/com/google/gerrit/index/query/AndSourceTest.java
+++ b/javatests/com/google/gerrit/index/query/AndSourceTest.java
@@ -29,7 +29,7 @@
TestDataSourcePredicate p1 = new TestDataSourcePredicate("predicate1", "foo", 10, 10);
TestDataSourcePredicate p2 = new TestDataSourcePredicate("predicate2", "foo", 1, 10);
AndSource<String> andSource = new AndSource<>(Lists.newArrayList(p1, p2), null);
- andSource.match("bar");
+ assertFalse(andSource.match("bar"));
assertFalse(p1.ranMatch);
assertTrue(p2.ranMatch);
}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
index c5e8574..00272112 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.Patch.FileMode;
import com.google.gerrit.entities.Patch.PatchType;
import com.google.gerrit.server.patch.ComparisonType;
import com.google.gerrit.server.patch.filediff.Edit;
@@ -42,6 +43,8 @@
.comparisonType(ComparisonType.againstOtherPatchSet())
.oldPath(Optional.of("old_file_path.txt"))
.newPath(Optional.empty())
+ .oldMode(Optional.of(FileMode.REGULAR_FILE))
+ .newMode(Optional.of(FileMode.SYMLINK))
.changeType(ChangeType.DELETED)
.patchType(Optional.of(PatchType.UNIFIED))
.size(23)
diff --git a/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java b/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
index 96919be..7bdb23c 100644
--- a/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
+++ b/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.extensions.events;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
@@ -22,6 +23,7 @@
import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import java.io.IOException;
+import java.time.Instant;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -41,10 +43,12 @@
private DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners;
private DynamicSet<GitBatchRefUpdateListener> batchRefUpdateListeners;
+ private final AccountState updater =
+ AccountState.forAccount(Account.builder(Account.id(1), Instant.now()).build());
+
@Mock GitReferenceUpdatedListener refUpdatedListener;
@Mock GitBatchRefUpdateListener batchRefUpdateListener;
@Mock EventUtil util;
- @Mock AccountState updater;
@Before
public void setup() {
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index 11f3528..2d90ab4 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -75,6 +75,7 @@
allUsersRepo.close();
}
+ @Nullable
protected Instant getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
try (RevWalk rw = new RevWalk(allUsersRepo)) {
Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index e35941c..0bdf5cd 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -157,16 +157,14 @@
@Test
public void tolerateNullValuesForInsertion() {
Project.NameKey project = Project.nameKey("project");
- ChangeData cd =
- ChangeData.createForTest(project, Change.id(1), 1, ObjectId.zeroId(), null, null, null);
+ ChangeData cd = ChangeData.createForTest(project, Change.id(1), 1, ObjectId.zeroId());
assertThat(ChangeField.ADDED.setIfPossible(cd, new FakeStoredValue(null))).isTrue();
}
@Test
public void tolerateNullValuesForDeletion() {
Project.NameKey project = Project.nameKey("project");
- ChangeData cd =
- ChangeData.createForTest(project, Change.id(1), 1, ObjectId.zeroId(), null, null, null);
+ ChangeData cd = ChangeData.createForTest(project, Change.id(1), 1, ObjectId.zeroId());
assertThat(ChangeField.DELETED.setIfPossible(cd, new FakeStoredValue(null))).isTrue();
}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index bcd0add..26e9e54 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -130,7 +130,7 @@
assertThat(out.getChild(0)).isEqualTo(query(firstIndexedSubQuery));
assertThat(out.getChild(1).getClass()).isSameInstanceAs(OrPredicate.class);
- OrPredicate indexedSubTree = (OrPredicate) out.getChild(1);
+ OrPredicate<ChangeData> indexedSubTree = (OrPredicate<ChangeData>) out.getChild(1);
Predicate<ChangeData> secondIndexedSubQuery = parse("foo:a OR file:b");
assertThat(indexedSubTree.getChildren())
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index f70c97a..2aa9ca4 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -16,7 +16,10 @@
import static com.google.gerrit.index.SchemaUtil.schema;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.FieldBundle;
@@ -29,10 +32,21 @@
@Ignore
public class FakeChangeIndex implements ChangeIndex {
- static final Schema<ChangeData> V1 = schema(1, ChangeField.STATUS);
+ static final Schema<ChangeData> V1 =
+ schema(
+ 1,
+ ImmutableList.<FieldDef<ChangeData, ?>>of(),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.STATUS_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.STATUS_SPEC));
static final Schema<ChangeData> V2 =
- schema(2, ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED);
+ schema(
+ 2,
+ ImmutableList.<FieldDef<ChangeData, ?>>of(ChangeField.UPDATED),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(
+ ChangeField.PATH_FIELD, ChangeField.STATUS_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.PATH_SPEC, ChangeField.STATUS_SPEC));
private static class Source implements ChangeDataSource {
private final Predicate<ChangeData> p;
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index e8cc6b4..1b286d1 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -18,6 +18,7 @@
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
@@ -178,9 +179,9 @@
install(NoteDbModule.forTest());
bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
bind(String.class).annotatedWith(GerritServerId.class).toInstance(serverId);
- bind(new TypeLiteral<ImmutableList<String>>() {})
+ bind(new TypeLiteral<ImmutableSet<String>>() {})
.annotatedWith(GerritImportedServerIds.class)
- .toInstance(new ImmutableList.Builder<String>().add(importedServerIds).build());
+ .toInstance(new ImmutableSet.Builder<String>().add(importedServerIds).build());
bind(GitRepositoryManager.class).toInstance(repoManager);
bind(ProjectCache.class).to(NullProjectCache.class);
bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 4edfa8b4..61b5e55 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -37,6 +37,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.AttentionSetUpdate;
@@ -3950,6 +3951,7 @@
return new String(rw.getObjectReader().open(dataId, OBJ_BLOB).getCachedBytes(), UTF_8);
}
+ @Nullable
private ObjectId exactRefAllUsers(String refName) throws Exception {
try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
Ref ref = allUsersRepo.exactRef(refName);
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index 2191f00..5a89584 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -123,6 +123,18 @@
}
@Test
+ public void fixedFallbackFormatCanParseOutputOfLegacyAdapter() {
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 7, 2017 2:20:30 AM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-07T10:20:30Z").toInstant()));
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 17, 2017 10:20:30 AM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-17T18:20:30Z").toInstant()));
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 17, 2017 02:20:30 PM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-17T22:20:30Z").toInstant()));
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 07, 2017 10:20:30 PM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-08T06:20:30Z").toInstant()));
+ }
+
+ @Test
public void newAdapterDisagreesWithLegacyAdapterDuringDstTransition() {
String duringJson = legacyGson.toJson(new Timestamp(MID_DST_MS));
Timestamp duringTs = legacyGson.fromJson(duringJson, Timestamp.class);
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index b0e705b..1fede32 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.fail;
@@ -24,6 +25,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.GerritApi;
@@ -32,10 +34,12 @@
import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
+import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -277,6 +281,7 @@
@Test
public void byUsername() throws Exception {
+ assume().that(hasIndexByUsername()).isTrue();
AccountInfo user1 = newAccount("myuser");
assertQuery("notexisting");
@@ -387,6 +392,46 @@
}
@Test
+ public void byCanSee_privateChange() throws Exception {
+ String domain = name("test.com");
+ AccountInfo user1 = newAccountWithEmail("account1", "account1@" + domain);
+ AccountInfo user2 = newAccountWithEmail("account2", "account2@" + domain);
+ AccountInfo user3 = newAccountWithEmail("account3", "account3@" + domain);
+ AccountInfo user4 = newAccountWithEmail("account4", "account4@" + domain);
+
+ Project.NameKey p = createProject(name("p"));
+
+ // Create the change as User1
+ requestContext.setContext(newRequestContext(Account.id(user1._accountId)));
+ ChangeInfo c = createPrivateChange(p);
+ assertThat(c.owner).isEqualTo(user1);
+
+ // Add user2 as a reviewer, user3 as a CC, and leave user4 dangling.
+ addReviewer(c.changeId, user2.email, ReviewerState.REVIEWER);
+ addReviewer(c.changeId, user3.email, ReviewerState.CC);
+
+ // Request as the owner
+ requestContext.setContext(newRequestContext(Account.id(user1._accountId)));
+ assertQuery("cansee:" + c.changeId, user1, user2, user3);
+
+ // Request as the reviewer
+ requestContext.setContext(newRequestContext(Account.id(user2._accountId)));
+ assertQuery("cansee:" + c.changeId, user1, user2, user3);
+
+ // Request as the CC
+ requestContext.setContext(newRequestContext(Account.id(user3._accountId)));
+ assertQuery("cansee:" + c.changeId, user1, user2, user3);
+
+ // Request as an account not in {owner, reviewer, CC}
+ requestContext.setContext(newRequestContext(Account.id(user4._accountId)));
+ BadRequestException exception =
+ assertThrows(BadRequestException.class, () -> newQuery("cansee:" + c.changeId).get());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(String.format("change %s not found", c.changeId));
+ }
+
+ @Test
public void byWatchedProject() throws Exception {
Project.NameKey p = createProject(name("p"));
Project.NameKey p2 = createProject(name("p2"));
@@ -517,7 +562,7 @@
public void withDetails() throws Exception {
AccountInfo user1 = newAccount("myuser", "My User", "my.user@example.com", true);
- List<AccountInfo> result = assertQuery(user1.username, user1);
+ List<AccountInfo> result = assertQuery(getDefaultSearch(user1), user1);
AccountInfo ai = result.get(0);
assertThat(ai._accountId).isEqualTo(user1._accountId);
assertThat(ai.name).isNull();
@@ -525,7 +570,9 @@
assertThat(ai.email).isNull();
assertThat(ai.avatars).isNull();
- result = assertQuery(newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
+ result =
+ assertQuery(
+ newQuery(getDefaultSearch(user1)).withOption(ListAccountsOption.DETAILS), user1);
ai = result.get(0);
assertThat(ai._accountId).isEqualTo(user1._accountId);
assertThat(ai.name).isEqualTo(user1.name);
@@ -540,25 +587,29 @@
String[] secondaryEmails = new String[] {"bar@example.com", "foo@example.com"};
addEmails(user1, secondaryEmails);
- List<AccountInfo> result = assertQuery(user1.username, user1);
+ List<AccountInfo> result = assertQuery(getDefaultSearch(user1), user1);
assertThat(result.get(0).secondaryEmails).isNull();
- result = assertQuery(newQuery(user1.username).withSuggest(true), user1);
- assertThat(result.get(0).secondaryEmails)
- .containsExactlyElementsIn(Arrays.asList(secondaryEmails))
- .inOrder();
-
- result = assertQuery(newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
- assertThat(result.get(0).secondaryEmails).isNull();
-
- result = assertQuery(newQuery(user1.username).withOption(ListAccountsOption.ALL_EMAILS), user1);
+ result = assertQuery(newQuery(getDefaultSearch(user1)).withSuggest(true), user1);
assertThat(result.get(0).secondaryEmails)
.containsExactlyElementsIn(Arrays.asList(secondaryEmails))
.inOrder();
result =
assertQuery(
- newQuery(user1.username)
+ newQuery(getDefaultSearch(user1)).withOption(ListAccountsOption.DETAILS), user1);
+ assertThat(result.get(0).secondaryEmails).isNull();
+
+ result =
+ assertQuery(
+ newQuery(getDefaultSearch(user1)).withOption(ListAccountsOption.ALL_EMAILS), user1);
+ assertThat(result.get(0).secondaryEmails)
+ .containsExactlyElementsIn(Arrays.asList(secondaryEmails))
+ .inOrder();
+
+ result =
+ assertQuery(
+ newQuery(getDefaultSearch(user1))
.withOptions(ListAccountsOption.DETAILS, ListAccountsOption.ALL_EMAILS),
user1);
assertThat(result.get(0).secondaryEmails)
@@ -576,21 +627,22 @@
requestContext.setContext(newRequestContext(Account.id(user._accountId)));
- List<AccountInfo> result = newQuery(otherUser.username).withSuggest(true).get();
+ List<AccountInfo> result = newQuery(getDefaultSearch(otherUser)).withSuggest(true).get();
assertThat(result.get(0).secondaryEmails).isNull();
assertThrows(
AuthException.class,
- () -> newQuery(otherUser.username).withOption(ListAccountsOption.ALL_EMAILS).get());
+ () ->
+ newQuery(getDefaultSearch(otherUser)).withOption(ListAccountsOption.ALL_EMAILS).get());
}
@Test
public void asAnonymous() throws Exception {
- AccountInfo user1 = newAccount("user1");
+ AccountInfo user1 = newAccount("user1", "user1@gerrit.com", /*active=*/ true);
setAnonymous();
assertQuery("9999999");
assertQuery("self");
- assertQuery("username:" + user1.username, user1);
+ assertQuery("email:" + user1.email, user1);
}
// reindex permissions are tested by {@link AccountIT#reindexPermissions}
@@ -631,7 +683,12 @@
.getRaw(
Account.id(userInfo._accountId),
QueryOptions.create(
- IndexConfig.fromConfig(config).build(), 0, 1, schema.getStoredFields()));
+ config != null
+ ? IndexConfig.fromConfig(config).build()
+ : IndexConfig.createDefault(),
+ 0,
+ 1,
+ schema.getStoredFields()));
assertThat(rawFields).isPresent();
if (schema.hasField(AccountField.ID_FIELD_SPEC)) {
@@ -649,6 +706,11 @@
assertThat(extId).isPresent();
blobs.add(new ByteArrayWrapper(extId.get().toByteArray()));
}
+
+ // Some installations do not store EXTERNAL_ID_STATE_SPEC
+ if (!schema.hasField(AccountField.EXTERNAL_ID_STATE_SPEC)) {
+ return;
+ }
Iterable<byte[]> externalIdStates =
rawFields.get().<Iterable<byte[]>>getValue(AccountField.EXTERNAL_ID_STATE_SPEC);
assertThat(externalIdStates).hasSize(blobs.size());
@@ -656,6 +718,21 @@
.containsExactlyElementsIn(blobs);
}
+ private String getDefaultSearch(AccountInfo user) {
+ return hasIndexByUsername() ? user.username : user.name;
+ }
+
+ /**
+ * Returns 'true' is {@link AccountField#USERNAME_FIELD} is indexed.
+ *
+ * <p>Some installations do not index {@link AccountField#USERNAME_FIELD}, since they do not use
+ * {@link ExternalId#SCHEME_USERNAME}
+ */
+ private boolean hasIndexByUsername() {
+ Schema<AccountState> schema = indexes.getSearchIndex().getSchema();
+ return schema.hasField(AccountField.USERNAME_SPEC);
+ }
+
protected AccountInfo newAccount(String username) throws Exception {
return newAccountWithEmail(username, null);
}
@@ -709,6 +786,15 @@
gApi.projects().name(project.get()).access(in);
}
+ protected ChangeInfo createPrivateChange(Project.NameKey project) throws RestApiException {
+ ChangeInput in = new ChangeInput();
+ in.subject = "A change";
+ in.project = project.get();
+ in.branch = "master";
+ in.isPrivate = true;
+ return gApi.changes().create(in).get();
+ }
+
protected ChangeInfo createChange(Project.NameKey project) throws RestApiException {
ChangeInput in = new ChangeInput();
in.subject = "A change";
@@ -717,6 +803,14 @@
return gApi.changes().create(in).get();
}
+ protected void addReviewer(String changeId, String email, ReviewerState state)
+ throws RestApiException {
+ ReviewerInput reviewerInput = new ReviewerInput();
+ reviewerInput.reviewer = email;
+ reviewerInput.state = state;
+ gApi.changes().id(changeId).addReviewer(reviewerInput);
+ }
+
protected GroupInfo createGroup(String name, AccountInfo... members) throws RestApiException {
GroupInput in = new GroupInput();
in.name = name;
@@ -742,6 +836,7 @@
return "\"" + s + "\"";
}
+ @Nullable
protected String name(String name) {
if (name == null) {
return null;
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index c255f5d..c781d8b 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -13,6 +13,7 @@
"//prolog:gerrit-prolog-common",
],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 9be7772..28149d8 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -626,7 +626,7 @@
@Test
public void byUploader() throws Exception {
- assume().that(getSchema().hasField(ChangeField.UPLOADER)).isTrue();
+ assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
CurrentUser user2CurrentUser = userFactory.create(user2);
@@ -762,7 +762,7 @@
@Test
public void byUploaderIn() throws Exception {
- assume().that(getSchema().hasField(ChangeField.UPLOADER)).isTrue();
+ assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo), userId);
assertQuery("uploaderin:Administrators", change1);
@@ -2015,7 +2015,7 @@
@Test
public void byMergedBefore() throws Exception {
- assume().that(getSchema().hasField(ChangeField.MERGED_ON)).isTrue();
+ assume().that(getSchema().hasField(ChangeField.MERGED_ON_SPEC)).isTrue();
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
// Stop the clock, will set time to specific test values.
@@ -2075,7 +2075,7 @@
@Test
public void byMergedAfter() throws Exception {
- assume().that(getSchema().hasField(ChangeField.MERGED_ON)).isTrue();
+ assume().that(getSchema().hasField(ChangeField.MERGED_ON_SPEC)).isTrue();
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
// Stop the clock, will set time to specific test values.
@@ -2145,7 +2145,7 @@
@Test
public void updatedThenMergedOrder() throws Exception {
- assume().that(getSchema().hasField(ChangeField.MERGED_ON)).isTrue();
+ assume().that(getSchema().hasField(ChangeField.MERGED_ON_SPEC)).isTrue();
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
// Stop the clock, will set time to specific test values.
@@ -3844,7 +3844,7 @@
@Test
public void isPureRevert() throws Exception {
- assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT)).isTrue();
+ assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT_SPEC)).isTrue();
TestRepository<Repo> repo = createProject("repo");
// Create two commits and revert second commit (initial commit can't be reverted)
Change initial = insert(repo, newChange(repo));
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index 5124021..e48d4af 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -15,29 +15,18 @@
package com.google.gerrit.server.query.change;
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.TestChanges;
-import java.util.UUID;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-@RunWith(MockitoJUnitRunner.class)
public class ChangeDataTest {
- private static final String GERRIT_SERVER_ID = UUID.randomUUID().toString();
-
- @Mock private ChangeNotes changeNotesMock;
-
@Test
public void setPatchSetsClearsCurrentPatchSet() throws Exception {
Project.NameKey project = Project.nameKey("project");
@@ -52,26 +41,6 @@
assertThat(curr2).isNotSameInstanceAs(curr1);
}
- @Test
- public void getChangeVirtualIdUsingAlgorithm() throws Exception {
- Project.NameKey project = Project.nameKey("project");
- final int encodedChangeNum = 12345678;
-
- when(changeNotesMock.getServerId()).thenReturn(UUID.randomUUID().toString());
-
- ChangeData cd =
- ChangeData.createForTest(
- project,
- Change.id(1),
- 1,
- ObjectId.zeroId(),
- GERRIT_SERVER_ID,
- (s, c) -> encodedChangeNum,
- changeNotesMock);
-
- assertThat(cd.getVirtualId().get()).isEqualTo(encodedChangeNum);
- }
-
private static PatchSet newPatchSet(Change.Id changeId, int num) {
return PatchSet.builder()
.id(PatchSet.id(changeId, num))
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index d69fe9e..fe60119 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -32,6 +32,7 @@
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.junit.Test;
@@ -55,7 +56,6 @@
@Test
@UseClockStep
- @SuppressWarnings("unchecked")
public void stopQueryIfNoMoreResults() throws Exception {
// create 2 visible changes
TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
@@ -72,7 +72,8 @@
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
- AbstractFakeIndex idx = (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
+ AbstractFakeIndex<?, ?, ?> idx =
+ (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
newQuery("status:new").withLimit(5).get();
// Since the limit of the query (i.e. 5) is more than the total number of changes (i.e. 4),
// only 1 index search is expected.
@@ -81,7 +82,6 @@
@Test
@UseClockStep
- @SuppressWarnings("unchecked")
public void noLimitQueryPaginates() throws Exception {
TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
// create 4 changes
@@ -97,7 +97,8 @@
.add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 2))
.update();
- AbstractFakeIndex idx = (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
+ AbstractFakeIndex<?, ?, ?> idx =
+ (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
// 2 index searches are expected. The first index search will run with size 3 (i.e.
// the configured query-limit+1), and then we will paginate to get the remaining
@@ -108,7 +109,6 @@
@Test
@UseClockStep
- @SuppressWarnings("unchecked")
public void internalQueriesPaginate() throws Exception {
// create 4 changes
TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
@@ -124,12 +124,14 @@
.add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 2))
.update();
- AbstractFakeIndex idx = (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
+ AbstractFakeIndex<?, ?, ?> idx =
+ (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
// 2 index searches are expected. The first index search will run with size 3 (i.e.
// the configured query-limit+1), and then we will paginate to get the remaining
// changes with the second index search.
- queryProvider.get().query(queryBuilder.parse("status:new"));
+ List<ChangeData> matches = queryProvider.get().query(queryBuilder.parse("status:new"));
+ assertThat(matches).hasSize(4);
assertThat(idx.getQueryCount()).isEqualTo(2);
}
}
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 1ca4571..540416f 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.fail;
import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
@@ -382,7 +383,9 @@
.getRaw(
uuid,
QueryOptions.create(
- IndexConfig.fromConfig(config).build(),
+ config != null
+ ? IndexConfig.fromConfig(config).build()
+ : IndexConfig.createDefault(),
0,
10,
indexes.getSearchIndex().getSchema().getStoredFields()));
@@ -558,6 +561,7 @@
return groups.stream().map(g -> g.id).sorted().collect(toList());
}
+ @Nullable
protected String name(String name) {
if (name == null) {
return null;
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 0cc132d..e877c81 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -10,6 +10,7 @@
visibility = ["//visibility:public"],
runtime_deps = ["//java/com/google/gerrit/lucene"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index c06fcde..b119104 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -24,6 +24,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.GerritApi;
@@ -469,6 +470,7 @@
return projects.stream().map(p -> p.name).collect(toList());
}
+ @Nullable
protected String name(String name) {
if (name == null) {
return null;
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index f476ae6..53f9d9d 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -10,6 +10,7 @@
visibility = ["//visibility:public"],
runtime_deps = ["//java/com/google/gerrit/lucene"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/util/http/testutil/BUILD b/javatests/com/google/gerrit/util/http/testutil/BUILD
index 3a67d45..3b4817b 100644
--- a/javatests/com/google/gerrit/util/http/testutil/BUILD
+++ b/javatests/com/google/gerrit/util/http/testutil/BUILD
@@ -6,6 +6,7 @@
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/common:annotations",
"//lib:guava",
"//lib:jgit",
"//lib:servlet-api",
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index ebdf2d9..0347177 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -25,6 +25,7 @@
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@@ -105,6 +106,7 @@
return -1;
}
+ @Nullable
@Override
public String getContentType() {
List<String> contentType = headers.get("Content-Type");
diff --git a/package.json b/package.json
index cdaf400..af1354b 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"typescript": "^4.7.2"
},
"scripts": {
+ "setup": "yarn && yarn --cwd=polygerrit-ui && yarn --cwd=polygerrit-ui/app",
"clean": "git clean -fdx && bazel clean --expunge",
"compile:local": "tsc --project ./polygerrit-ui/app/tsconfig.json",
"compile:watch": "npm run compile:local -- --preserveWatchOutput --watch",
@@ -41,6 +42,7 @@
"test": "yarn --cwd=polygerrit-ui test",
"test:screenshot": "yarn --cwd=polygerrit-ui test:screenshot",
"test:screenshot-update": "yarn --cwd=polygerrit-ui test:screenshot-update",
+ "test:browsers": "yarn --cwd=polygerrit-ui test:browsers",
"test:coverage": "yarn --cwd=polygerrit-ui test:coverage",
"test:watch": "yarn --cwd=polygerrit-ui test:watch",
"test:single": "yarn --cwd=polygerrit-ui test:single",
diff --git a/plugins/download-commands b/plugins/download-commands
index 71331e1..a16ebc6 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 71331e15af5a62ee7b13dee6ebdadf23d7e75a40
+Subproject commit a16ebc6cdaaa4db5e5a2b6d062bb0ebbb3d3d0f4
diff --git a/plugins/gitiles b/plugins/gitiles
index 24529d2..12e26b3 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 24529d232268ac51fd6850770f70dc0fcd732dd8
+Subproject commit 12e26b33ac55109bbb1d5eb56f198235552fb919
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 4198fe8..10db2cf 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 4198fe8df1c1b86d812f32da63e891b1c2fc6f3e
+Subproject commit 10db2cf772989d031c6f3558010c51fe07cf9722
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 3239ce3..084a372 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 3239ce3a471f5aa9edd8f6f702bee655ea81f77d
+Subproject commit 084a37253dc94ac52cfaa1c9d516fcb8b0318b31
diff --git a/plugins/webhooks b/plugins/webhooks
index d8815bf..16110f3 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit d8815bf9660b6655696db242b8ad2801e866c036
+Subproject commit 16110f320dd5b6a40af87eaba4bf3af60cb0efd1
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index e012bd1..ec05cad 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -10,9 +10,9 @@
"@babel/highlight" "^7.18.6"
"@babel/helper-validator-identifier@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
- integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
+ version "7.19.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+ integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/highlight@^7.18.6":
version "7.18.6"
@@ -35,16 +35,11 @@
resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.7.0.tgz#ae3886b5c4ddc6a02659a11d577e1df0b6158727"
integrity sha512-8zeZClN1gur+Isrn02bRXJ0wUjYvK99jQxg36ZbDelrGDglXKddf8QQkZmaH9sYIRcCFDLlh5+ZlRUTcXTuDVA==
-"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.4.0":
+"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"
integrity sha512-qDv4851VFSaBWzpS02cXHclo40jsbAjRXnebNXpm0uVg32kCneZPo9RYVQtrTNICtZ+1wAYHu1ZtxWSWMbKrBw==
-"@lit/reactive-element@^1.3.0":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.3.2.tgz#43e470537b6ec2c23510c07812616d5aa27a17cd"
- integrity sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==
-
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -131,9 +126,9 @@
"@polymer/polymer" "^3.0.5"
"@polymer/polymer@^3.0.5", "@polymer/polymer@^3.4.1":
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.4.1.tgz#333bef25711f8411bb5624fb3eba8212ef8bee96"
- integrity sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.5.1.tgz#4b5234e43b8876441022bcb91313ab3c4a29f0c8"
+ integrity sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==
dependencies:
"@webcomponents/shadycss" "^1.9.1"
@@ -289,9 +284,9 @@
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
"@types/node@*":
- version "18.7.18"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154"
- integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==
+ version "18.8.3"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c"
+ integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==
"@types/parse5@^6.0.1":
version "6.0.3"
@@ -1070,44 +1065,28 @@
vary "^1.1.2"
lit-element@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.0.tgz#9c981c55dfd9a8f124dc863edb62cc529d434db7"
- integrity sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.2.tgz#d148ab6bf4c53a33f707a5168e087725499e5f2b"
+ integrity sha512-6ZgxBR9KNroqKb6+htkyBwD90XGRiqKDHVrW/Eh0EZ+l+iC+u+v+w3/BA5NGi4nizAVHGYvQBHUDuSmLjPp7NQ==
dependencies:
"@lit/reactive-element" "^1.3.0"
lit-html "^2.2.0"
-lit-html@^2.0.0, lit-html@^2.3.0:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.3.1.tgz#56f15104ea75c0a702904893e3409d0e89e2a2b9"
- integrity sha512-FyKH6LTW6aBdkfNhNSHyZTnLgJSTe5hMk7HFtc/+DcN1w74C215q8B+Cfxc2OuIEpBNcEKxgF64qL8as30FDHA==
+lit-html@^2.0.0, lit-html@^2.2.0, lit-html@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.4.0.tgz#b510430f39a56ec959167ed1187241a4e3ab1574"
+ integrity sha512-G6qXu4JNUpY6aaF2VMfaszhO9hlWw0hOTRFDmuMheg/nDYGB+2RztUSOyrzALAbr8Nh0Y7qjhYkReh3rPnplVg==
dependencies:
"@types/trusted-types" "^2.0.2"
-lit-html@^2.2.0:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.2.6.tgz#e70679605420a34c4f3cbd0c483b2fb1fff781df"
- integrity sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==
- dependencies:
- "@types/trusted-types" "^2.0.2"
-
-lit@^2.0.0:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.3.1.tgz#2cf1c2042da1e44c7a7cc72dff2d72303fd26f48"
- integrity sha512-TejktDR4mqG3qB32Y8Lm5Lye3c8SUehqz7qRsxe1PqGYL6me2Ef+jeQAEqh20BnnGncv4Yxy2njEIT0kzK1WCw==
+lit@^2.0.0, lit@^2.2.3:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.4.0.tgz#e33a0f463e17408f6e7f71464e6a266e60a5bb77"
+ integrity sha512-fdgzxEtLrZFQU/BqTtxFQCLwlZd9bdat+ltzSFjvWkZrs7eBmeX0L5MHUMb3kYIkuS8Xlfnii/iI5klirF8/Xg==
dependencies:
"@lit/reactive-element" "^1.4.0"
lit-element "^3.2.0"
- lit-html "^2.3.0"
-
-lit@^2.2.3:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.2.6.tgz#4ef223e88517c000b0c01baf2e3535e61a75a5b5"
- integrity sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==
- dependencies:
- "@lit/reactive-element" "^1.3.0"
- lit-element "^3.2.0"
- lit-html "^2.2.0"
+ lit-html "^2.4.0"
log-update@^4.0.0:
version "4.0.0"
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
index 6673cdf..d2b865b 100644
--- a/polygerrit-ui/FE_Style_Guide.md
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -125,10 +125,6 @@
Do not use getAppContext() anywhere else in a class.
-**Note:** This rule doesn't apply for HTML/Polymer elements classes. A browser creates instances of such classes
-implicitly and calls the constructor without parameters. See
-[Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)
-
**Good:**
```Javascript
export class UserService {
@@ -160,90 +156,3 @@
}
```
-
-## <a name="assign-dependencies-in-html-element-constructor"></a>Assign required services in a HTML/Polymer element constructor
-If a class is a custom HTML/Polymer element, the class must assign all required services in the constructor.
-A browser creates instances of such classes implicitly, so it is impossible to pass anything as a parameter to
-the element's class constructor.
-
-Do not use appContext anywhere except the constructor of the class.
-
-**Note for legacy elements:** If a polymer element extends a LegacyElementMixin and overrides the `created()` method,
-move all code from this method to a constructor right after the call to a `super()`
-([example](#assign-dependencies-legacy-element-example)). The `created()`
-method is [deprecated](https://polymer-library.polymer-project.org/2.0/docs/about_20#lifecycle-changes) and is called
-when a super (i.e. base) class constructor is called. If you are unsure about moving the code from the `created` method
-to the class constructor, consult with the source code:
-[`LegacyElementMixin._initializeProperties`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/legacy/legacy-element-mixin.js#L318)
-and
-[`PropertiesChanged.constructor`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/mixins/properties-changed.js#L177)
-
-
-
-**Good:**
-```Javascript
-import {appContext} from `.../services/app-context.js`;
-
-export class MyCustomElement extends ...{
- constructor() {
- super(); //This is mandatory to call parent constructor
- this._userModel = appContext.userModel;
- }
- //...
- _getUserName() {
- return this._userModel.activeUserName();
- }
-}
-```
-
-**Bad:**
-```Javascript
-import {appContext} from `.../services/app-context.js`;
-
-export class MyCustomElement extends ...{
- created() {
- // Incorrect: assign all dependencies in the constructor
- this._userModel = appContext.userModel;
- }
- //...
- _getUserName() {
- // Incorrect: use appContext outside of a constructor
- return appContext.userModel.activeUserName();
- }
-}
-```
-
-<a name="assign-dependencies-legacy-element-example"></a>
-**Legacy element:**
-
-Before:
-```Javascript
-export class MyCustomElement extends ...LegacyElementMixin(...) {
- constructor() {
- super();
- someAction();
- }
- created() {
- super();
- createdAction1();
- createdAction2();
- }
-}
-```
-
-After:
-```Javascript
-export class MyCustomElement extends ...LegacyElementMixin(...) {
- constructor() {
- super();
- // Assign services here
- this._userModel = appContext.userModel;
- // Code from the created method - put it before existing actions in constructor
- createdAction1();
- createdAction2();
- // Original constructor code
- someAction();
- }
- // created method is removed
-}
-```
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index ac8712b..510ce54 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -24,11 +24,13 @@
Follow the instructions
[here](https://gerrit-review.googlesource.com/Documentation/dev-bazel.html#_installation)
-to get and install Bazel.
+to get and install Bazel. The `npm install -g @bazel/bazelisk` method is
+probably easiest since you will have npm as part of Nodejs.
## Installing [Node.js](https://nodejs.org/en/download/) and npm packages
-The minimum nodejs version supported is 10.x+.
+The minimum nodejs version supported is 10.x+. We recommend at least the latest
+LTS (v16 as of October 2022).
```sh
# Debian experimental
@@ -80,11 +82,12 @@
## Setup typescript support in the IDE
-Modern IDE should automatically handle typescript settings from the
-`polygerrit-ui/app/tsconfig.json` files. IDE places compiled files in the
-`.ts-out/pg` directory at the root of gerrit workspace and you can configure IDE
-to exclude the whole .ts-out directory. To do it in the IntelliJ IDEA click on
-this directory and select "Mark Directory As > Excluded" in the context menu.
+Modern IDEs should automatically handle typescript settings from the
+`polygerrit-ui/app/tsconfig.json` files. The `tsc` compiler places compiled
+files in the `.ts-out/pg` directory at the root of gerrit workspace and you can
+configure the IDE to exclude the whole .ts-out directory. To do it in the
+IntelliJ IDEA click on this directory and select "Mark Directory As > Excluded"
+in the context menu.
However, if you receive some errors from IDE, you can try to configure IDE
manually. For example, if IntelliJ IDEA shows
@@ -92,22 +95,27 @@
options `--project polygerrit-ui/app/tsconfig.json` in the IDE settings.
-## Serving files locally
+## Developing locally
-#### Web Dev Server
+The preferred method for development is to serve the web files locally using the
+Web Dev Server and then view a running gerrit instance (local or otherwise) to
+replace its web client with the local one using the Gerrit FE Dev Helper
+extension.
-To test the local frontend against production data or a local test site execute:
+### Web Dev Server
+
+The [Web Dev Server](https://modern-web.dev/docs/dev-server/overview/) serves
+the compiled web files and dependencies unbundled over localhost. Start it using
+this command:
```sh
yarn start
```
-This command starts the [Web Dev Server](https://modern-web.dev/docs/dev-server/overview/).
To inject plugins or other files, we use the [Gerrit FE Dev Helper](https://chrome.google.com/webstore/detail/gerrit-fe-dev-helper/jimgomcnodkialnpmienbomamgomglkd) Chrome extension.
If any issues occured, please refer to the Troubleshooting section at the bottom or contact the team!
-## Running locally against production data
### Chrome extension: Gerrit FE Dev Helper
@@ -120,7 +128,7 @@
To use this extension, just follow its [readme here](https://gerrit.googlesource.com/gerrit-fe-dev-helper/+/master/README.md).
-## Running locally against a Gerrit test site
+### Running locally against a Gerrit test site
Set up a local test site once:
@@ -144,26 +152,38 @@
--dev-cdn http://localhost:8081
```
+The Web Dev Server is currently not serving fonts or other static assets. Follow
+[Issue 16341](https://bugs.chromium.org/p/gerrit/issues/detail?id=16341) for
+fixing this issue.
+
*NOTE* You can use any other cdn here, for example: https://cdn.googlesource.com/polygerrit_ui/678.0
## Running Tests
For daily development you typically only want to run and debug individual tests.
-There are several ways to run tests.
+Our tests run using the
+[Web Test Runner](https://modern-web.dev/docs/test-runner/overview/). There are
+several ways to trigger tests:
-* Run all tests:
+* Run all tests once:
```sh
yarn test
```
-* Run all tests under bazel:
+* Run all tests and then watches for changes. Change a file will trigger all
+tests affected by the changes.
+```sh
+yarn test:watch
+```
+
+* Run all tests once under bazel:
```sh
./polygerrit-ui/app/run_test.sh
```
-* Run a single test file:
+* Run a single test file and rerun on any changes affecting it:
```
-yarn test:single "**/async-foreach-behavior_test.js"
+yarn test:single "**/gr-comment_test.ts"
```
Compiling code:
@@ -172,34 +192,9 @@
yarn compile:local
# Watch mode:
-## Terminal 1:
yarn compile:watch
-## Terminal 2, test & watch a file for example:
-yarn test:single "**/async-foreach-behavior_test.js"
```
-### Generated file overview
-
-A generated file starts with imports followed by a static content with
-different type definitions. You can skip this part - it doesn't contains
-anything usefule.
-
-After the static content there is a class definition. Example:
-```typescript
-export class GrCreateGroupDialogCheck extends GrCreateGroupDialog {
- templateCheck() {
- // Converted template
- // Each HTML element from the template is wrapped into own block.
- }
-}
-```
-
-The converted template usually quite straightforward, but in some cases
-additional functions are added. For example, `<element x=[[y.a]]>` converts into
-`el.x = y!.a` if y is a simple type. However, if y has a union type, like - `y:A|B`,
-then the generated code looks like `el.x=__f(y)!.a` (`y!.a` may result in a TS error
-if `a` is defined only in one type of a union).
-
## Style guide
We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 330e616..6df4456 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -203,7 +203,12 @@
"--rules.no-property-visibility-mismatch off",
"--rules.no-incompatible-property-type off",
"--rules.no-incompatible-type-binding off",
- "--rules.no-unknown-attribute error",
+ # TODO: We would actually like to change this to `error`, but we also
+ # want to allow certain attributes, for example `aria-description`. This
+ # would be possible, if we would run the lit-analyzer as a ts plugin.
+ # In tsconfig.json there is an option `globalAttributes` that we could
+ # use. But that is not available when running lit-analyzer as cli.
+ "--rules.no-unknown-attribute warn",
],
)
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index b9c065f..ef70a75 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -22,19 +22,16 @@
// Note: for new events, naming convention should be: `a-b`
export enum EventType {
- HISTORY = 'history',
LABEL_CHANGE = 'labelchange',
SHOW_CHANGE = 'showchange',
SUBMIT_CHANGE = 'submitchange',
SHOW_REVISION_ACTIONS = 'show-revision-actions',
COMMIT_MSG_EDIT = 'commitmsgedit',
- COMMENT = 'comment',
REVERT = 'revert',
REVERT_SUBMISSION = 'revert_submission',
POST_REVERT = 'postrevert',
ANNOTATE_DIFF = 'annotatediff',
ADMIN_MENU_LINKS = 'admin-menu-links',
- HIGHLIGHTJS_LOADED = 'highlightjs-loaded',
}
export declare interface PluginApi {
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 3c06eb0..1768552 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -142,9 +142,9 @@
}
/**
- * The state of the projects
+ * The state of the repository
*/
-export enum ProjectState {
+export enum RepoState {
ACTIVE = 'ACTIVE',
READ_ONLY = 'READ_ONLY',
HIDDEN = 'HIDDEN',
@@ -489,7 +489,7 @@
/**
* The ConfigInfo entity contains information about the effective
- * project configuration.
+ * repository configuration.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-info
*/
export declare interface ConfigInfo {
@@ -508,7 +508,7 @@
default_submit_type: SubmitTypeInfo;
submit_type: SubmitType;
match_author_to_committer_date?: InheritedBooleanInfo;
- state?: ProjectState;
+ state?: RepoState;
commentlinks: CommentLinks;
plugin_config?: PluginNameToPluginParametersMap;
actions?: {[viewName: string]: ActionInfo};
@@ -619,6 +619,8 @@
lines_deleted?: number;
size_delta?: number; // in bytes
size?: number; // in bytes
+ old_mode?: number;
+ new_mode?: number;
}
/**
@@ -740,7 +742,7 @@
export type LabelNameToLabelTypeInfoMap = {[labelName: string]: LabelTypeInfo};
/**
- * The LabelTypeInfo entity contains metadata about the labels that a project
+ * The LabelTypeInfo entity contains metadata about the labels that a repository
* has.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#label-type-info
*/
@@ -756,7 +758,7 @@
/**
* The MaxObjectSizeLimitInfo entity contains information about the max object
- * size limit of a project.
+ * size limit of a repository.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#max-object-size-limit-info
*/
export declare interface MaxObjectSizeLimitInfo {
@@ -835,23 +837,23 @@
}
/**
- * The ProjectInfo entity contains information about a project
+ * The ProjectInfo entity contains information about a repository
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info
*/
export declare interface ProjectInfo {
id: UrlEncodedRepoName;
- // name is not set if returned in a map where the project name is used as
+ // name is not set if returned in a map where the repo name is used as
// map key
name?: RepoName;
- // ?-<n> if the parent project is not visible (<n> is a number which
- // is increased for each non-visible project).
+ // ?-<n> if the parent repository is not visible (<n> is a number which
+ // is increased for each non-visible repository).
parent?: RepoName;
description?: string;
- state?: ProjectState;
+ state?: RepoState;
branches?: {[branchName: string]: CommitId};
- // labels is filled for Create Project and Get Project calls.
+ // labels is filled for Create Repo and Get Repo calls.
labels?: LabelNameToLabelTypeInfoMap;
- // Links to the project in external sites
+ // Links to the repository in external sites
web_links?: WebLinkInfo[];
}
@@ -1005,8 +1007,8 @@
// where "'ffffffffff'" represents nanoseconds.
/**
- * Information about the default submittype of a project, taking into account
- * project inheritance.
+ * Information about the default submittype of a repository, taking into account
+ * repository inheritance.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-type-info
*/
export declare interface SubmitTypeInfo {
@@ -1169,7 +1171,7 @@
/**
* The label is required for submission, but is impossible to complete.
* The likely cause is access has not been granted correctly by the
- * project owner or site administrator.
+ * repository owner or site administrator.
*/
IMPOSSIBLE = 'IMPOSSIBLE',
OPTIONAL = 'OPTIONAL',
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index bb7b313..89c7622 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -22,7 +22,7 @@
InheritedBooleanInfoConfiguredValue,
MergeabilityComputationBehavior,
ProblemInfoStatus,
- ProjectState,
+ RepoState,
RequirementStatus,
ReviewerState,
RevisionKind,
@@ -41,7 +41,7 @@
InheritedBooleanInfoConfiguredValue,
MergeabilityComputationBehavior,
ProblemInfoStatus,
- ProjectState,
+ RepoState,
RequirementStatus,
ReviewerState,
RevisionKind,
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 0e00d07..ae4aad9 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -14,6 +14,8 @@
PLUGINS_INSTALLED = 'Plugins installed',
PLUGINS_FAILED = 'Some plugins failed to load',
USER_REFERRED_FROM = 'User referred from',
+ NOTIFICATION_PERMISSION = 'Notification Permission',
+ SERVICE_WORKER_UPDATE = 'Service worker update',
}
export enum Execution {
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index d5a83a7..ce0ea02 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -16,7 +16,7 @@
import {
EditablePermissionInfo,
PermissionAccessSection,
- EditableProjectAccessGroups,
+ EditableRepoAccessGroups,
} from '../gr-repo-access/gr-repo-access-interfaces';
import {
CapabilityInfoMap,
@@ -68,7 +68,7 @@
section?: PermissionAccessSection;
@property({type: Object})
- groups?: EditableProjectAccessGroups;
+ groups?: EditableRepoAccessGroups;
@property({type: Object})
labels?: LabelNameToLabelTypeInfoMap;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index 5d32d32..c472d4d 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -7,7 +7,6 @@
import '../../shared/gr-list-view/gr-list-view';
import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-group-dialog/gr-create-group-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
import {GrCreateGroupDialog} from '../gr-create-group-dialog/gr-create-group-dialog';
import {fireTitleChange} from '../../../utils/event-util';
@@ -20,6 +19,8 @@
import {assertIsDefined} from '../../../utils/common-util';
import {AdminViewState} from '../../../models/views/admin';
import {createGroupUrl} from '../../../models/views/group';
+import {whenVisible} from '../../../utils/dom-util';
+import {modalStyles} from '../../../styles/gr-modal-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -31,7 +32,7 @@
export class GrAdminGroupList extends LitElement {
readonly path = '/admin/groups';
- @query('#createOverlay') private createOverlay?: GrOverlay;
+ @query('#createModal') private createModal?: HTMLDialogElement;
@query('#createNewModal') private createNewModal?: GrCreateGroupDialog;
@@ -69,6 +70,7 @@
return [
tableStyles,
sharedStyles,
+ modalStyles,
css`
gr-list-view {
--generic-list-description-width: 70%;
@@ -110,7 +112,7 @@
</tbody>
</table>
</gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
id="createDialog"
class="confirmDialog"
@@ -128,7 +130,7 @@
></gr-create-group-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -156,7 +158,7 @@
paramsChanged() {
this.filter = this.params?.filter ?? '';
this.offset = Number(this.params?.offset ?? 0);
- this.maybeOpenCreateOverlay(this.params);
+ this.maybeOpenCreateModal(this.params);
return this.getGroups(this.filter, this.groupsPerPage, this.offset);
}
@@ -166,10 +168,10 @@
*
* private but used in test
*/
- maybeOpenCreateOverlay(params?: AdminViewState) {
+ maybeOpenCreateModal(params?: AdminViewState) {
if (params?.openCreateModal) {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.open();
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.showModal();
}
}
@@ -229,14 +231,15 @@
// private but used in test
handleCloseCreate() {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.close();
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.close();
}
// private but used in test
handleCreateClicked() {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.open().then(() => {
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.showModal();
+ whenVisible(this.createModal, () => {
assertIsDefined(this.createNewModal, 'createNewModal');
this.createNewModal.focus();
});
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
index e484489..1b128aa 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
@@ -15,7 +15,6 @@
import {GerritView} from '../../../services/router/router-model';
import {GrListView} from '../../shared/gr-list-view/gr-list-view';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {fixture, html, assert} from '@open-wc/testing';
import {AdminChildView, AdminViewState} from '../../../models/views/admin';
@@ -83,13 +82,7 @@
<tbody class="loading"></tbody>
</table>
</gr-list-view>
- <gr-overlay
- aria-hidden="true"
- id="createOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
class="confirmDialog"
confirm-label="Create"
@@ -104,7 +97,7 @@
</gr-create-group-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -127,18 +120,18 @@
assert.equal(element.groups.slice(0, SHOWN_ITEMS_COUNT).length, 25);
});
- test('maybeOpenCreateOverlay', () => {
- const overlayOpen = sinon.stub(
- queryAndAssert<GrOverlay>(element, '#createOverlay'),
- 'open'
+ test('maybeOpenCreateModal', () => {
+ const modalOpen = sinon.stub(
+ queryAndAssert<HTMLDialogElement>(element, '#createModal'),
+ 'showModal'
);
- element.maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- element.maybeOpenCreateOverlay(undefined);
- assert.isFalse(overlayOpen.called);
+ element.maybeOpenCreateModal();
+ assert.isFalse(modalOpen.called);
+ element.maybeOpenCreateModal(undefined);
+ assert.isFalse(modalOpen.called);
value.openCreateModal = true;
- element.maybeOpenCreateOverlay(value);
- assert.isTrue(overlayOpen.called);
+ element.maybeOpenCreateModal(value);
+ assert.isTrue(modalOpen.called);
});
});
@@ -205,9 +198,10 @@
});
test('handleCreateClicked opens modal', () => {
- const openStub = sinon
- .stub(queryAndAssert<GrOverlay>(element, '#createOverlay'), 'open')
- .returns(Promise.resolve());
+ const openStub = sinon.stub(
+ queryAndAssert<HTMLDialogElement>(element, '#createModal'),
+ 'showModal'
+ );
element.handleCreateClicked();
assert.isTrue(openStub.called);
});
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 088002c..1c62114 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -19,7 +19,6 @@
import '../gr-repo-list/gr-repo-list';
import {getBaseUrl} from '../../../utils/url-util';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {
AdminNavLinksOption,
getAdminLinks,
@@ -34,7 +33,10 @@
} from '../../../types/common';
import {GroupNameChangedDetail} from '../gr-group/gr-group';
import {getAppContext} from '../../../services/app-context';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -59,6 +61,7 @@
} from '../../../models/views/repo';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
@@ -110,17 +113,17 @@
private reloading = false;
// private but used in the tests
- readonly jsAPI = getAppContext().jsApiService;
-
private readonly restApiService = getAppContext().restApiService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
private readonly getAdminViewModel = resolve(this, adminViewModelToken);
private readonly getGroupViewModel = resolve(this, groupViewModelToken);
private readonly getRepoViewModel = resolve(this, repoViewModelToken);
- private readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -152,7 +155,7 @@
);
subscribe(
this,
- () => this.routerModel.routerView$,
+ () => this.getRouterModel().routerView$,
view => {
this.view = view;
if (this.needsReload()) this.reload();
@@ -457,7 +460,7 @@
const promises: [Promise<AccountDetailInfo | undefined>, Promise<void>] =
[
this.restApiService.getAccount(),
- getPluginLoader().awaitPluginsLoaded(),
+ this.getPluginLoader().awaitPluginsLoaded(),
];
const result = await Promise.all(promises);
this.account = result[0];
@@ -487,7 +490,7 @@
}
return capabilities;
}),
- () => this.jsAPI.getAdminMenuLinks(),
+ () => this.getPluginLoader().jsApiService.getAdminMenuLinks(),
options
);
this.filteredLinks = res.links;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index d65d171..ee4a179 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -6,7 +6,6 @@
import '../../../test/common-test-setup';
import './gr-admin-view';
import {AdminSubsectionLink, GrAdminView} from './gr-admin-view';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {stubBaseUrl, stubElement, stubRestApi} from '../../../test/test-utils';
import {GerritView} from '../../../services/router/router-model';
import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
@@ -20,6 +19,10 @@
import {RepoDetailView} from '../../../models/views/repo';
import {testResolver} from '../../../test/common-test-setup';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {
+ PluginLoader,
+ pluginLoaderToken,
+} from '../../shared/gr-js-api-interface/gr-plugin-loader';
function createAdminCapabilities() {
return {
@@ -31,12 +34,14 @@
suite('gr-admin-view tests', () => {
let element: GrAdminView;
+ let pluginLoader: PluginLoader;
setup(async () => {
element = await fixture(html`<gr-admin-view></gr-admin-view>`);
stubRestApi('getProjectConfig').returns(Promise.resolve(undefined));
const pluginsLoaded = Promise.resolve();
- sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
+ pluginLoader = testResolver(pluginLoaderToken);
+ sinon.stub(pluginLoader, 'awaitPluginsLoaded').returns(pluginsLoaded);
await pluginsLoaded;
await element.updateComplete;
});
@@ -131,8 +136,12 @@
test('filteredLinks from plugin', () => {
stubRestApi('getAccount').returns(Promise.resolve(undefined));
- sinon.stub(element.jsAPI, 'getAdminMenuLinks').returns([
- {capability: null, text: 'internal link text', url: '/internal/link/url'},
+ sinon.stub(pluginLoader.jsApiService, 'getAdminMenuLinks').returns([
+ {
+ capability: null,
+ text: 'internal link text',
+ url: '/internal/link/url',
+ },
{
capability: null,
text: 'external link text',
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index 158419e..c88b1c5 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -204,11 +204,11 @@
}
private async getRepoSuggestions(input: string) {
- const response = await this.restApiService.getSuggestedProjects(input);
+ const response = await this.restApiService.getSuggestedRepos(input);
const repos = [];
- for (const [name, project] of Object.entries(response ?? {})) {
- repos.push({name, value: project.id});
+ for (const [name, repo] of Object.entries(response ?? {})) {
+ repos.push({name, value: repo.id});
}
return repos;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
index c716d65..87761a6 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -7,10 +7,8 @@
import '../../shared/gr-account-label/gr-account-label';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
-import '../../shared/gr-overlay/gr-overlay';
import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog';
import {getBaseUrl} from '../../../utils/url-util';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {
GroupId,
AccountId,
@@ -44,6 +42,7 @@
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
import {resolve} from '../../../models/dependency';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const SAVING_ERROR_TEXT =
'Group may not exist, or you may not have ' + 'permission to add it';
@@ -63,7 +62,7 @@
@customElement('gr-group-members')
export class GrGroupMembers extends LitElement {
- @query('#overlay') protected overlay!: GrOverlay;
+ @query('#modal') protected modal!: HTMLDialogElement;
@property({type: String})
groupId?: GroupId;
@@ -137,6 +136,7 @@
sharedStyles,
subpageStyles,
tableStyles,
+ modalStyles,
css`
.input {
width: 15em;
@@ -258,7 +258,7 @@
</div>
</div>
</div>
- <gr-overlay id="overlay" with-backdrop>
+ <dialog id="modal" tabindex="-1">
<gr-confirm-delete-item-dialog
class="confirmDialog"
.item=${this.itemName}
@@ -266,7 +266,7 @@
@confirm=${this.handleDeleteConfirm}
@cancel=${this.handleConfirmDialogCancel}
></gr-confirm-delete-item-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -411,7 +411,7 @@
if (!this.groupName) {
return Promise.reject(new Error('group name undefined'));
}
- this.overlay.close();
+ this.modal.close();
if (this.itemType === ItemType.MEMBER) {
return this.restApiService
.deleteGroupMember(this.groupName, this.itemId! as AccountId)
@@ -457,7 +457,7 @@
}
private handleConfirmDialogCancel() {
- this.overlay.close();
+ this.modal.close();
}
private handleDeleteMember(e: Event) {
@@ -472,7 +472,7 @@
this.itemName = item;
this.itemId = keys._account_id;
this.itemType = ItemType.MEMBER;
- this.overlay.open();
+ this.modal.showModal();
}
/* private but used in test */
@@ -525,7 +525,7 @@
this.itemName = item;
this.itemId = id;
this.itemType = ItemType.INCLUDED_GROUP;
- this.overlay.open();
+ this.modal.showModal();
}
/* private but used in test */
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
index 6c65dd6..0841595 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
@@ -346,16 +346,10 @@
</div>
</div>
</div>
- <gr-overlay
- aria-hidden="true"
- id="overlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="modal" tabindex="-1">
<gr-confirm-delete-item-dialog class="confirmDialog">
</gr-confirm-delete-item-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -540,12 +534,18 @@
deleteBtns[0].click();
assert.equal(element.itemId, 1000097 as AccountId);
assert.equal(element.itemName, 'jane');
+ queryAndAssert<HTMLDialogElement>(element, 'dialog').close();
+
deleteBtns[1].click();
assert.equal(element.itemId, 1000096 as AccountId);
assert.equal(element.itemName, 'Test User');
+ queryAndAssert<HTMLDialogElement>(element, 'dialog').close();
+
deleteBtns[2].click();
assert.equal(element.itemId, 1000095 as AccountId);
assert.equal(element.itemName, 'Gerrit');
+ queryAndAssert<HTMLDialogElement>(element, 'dialog').close();
+
deleteBtns[3].click();
assert.equal(element.itemId, 1000098 as AccountId);
assert.equal(element.itemName, '1000098');
@@ -559,9 +559,13 @@
deleteBtns[0].click();
assert.equal(element.itemId, 'testId' as GroupId);
assert.equal(element.itemName, 'testName');
+ queryAndAssert<HTMLDialogElement>(element, 'dialog').close();
+
deleteBtns[1].click();
assert.equal(element.itemId, 'testId2' as GroupId);
assert.equal(element.itemName, 'testName2');
+ queryAndAssert<HTMLDialogElement>(element, 'dialog').close();
+
deleteBtns[2].click();
assert.equal(element.itemId, 'testId3' as GroupId);
assert.equal(element.itemName, 'testName3');
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index 35e50ad..49fa99f 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -31,7 +31,7 @@
import {
EditablePermissionInfo,
EditablePermissionRuleInfo,
- EditableProjectAccessGroups,
+ EditableRepoAccessGroups,
} from '../gr-repo-access/gr-repo-access-interfaces';
import {getAppContext} from '../../../services/app-context';
import {fire, fireEvent} from '../../../utils/event-util';
@@ -88,7 +88,7 @@
permission?: PermissionArrayItem<EditablePermissionInfo>;
@property({type: Object})
- groups?: EditableProjectAccessGroups;
+ groups?: EditableRepoAccessGroups;
@property({type: String})
section?: GitRef;
@@ -458,7 +458,7 @@
}
computeGroupName(
- groups: EditableProjectAccessGroups | undefined,
+ groups: EditableRepoAccessGroups | undefined,
groupId: GitRef
) {
return groups && groups[groupId] && groups[groupId].name
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
index c7bd36fe..e9396b9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
@@ -66,6 +66,6 @@
export interface NewlyAddedGroupInfo {
name: string;
}
-export type EditableProjectAccessGroups = {
+export type EditableRepoAccessGroups = {
[uuid: string]: GroupInfo | NewlyAddedGroupInfo;
};
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 21ab184..27deb82 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -15,7 +15,7 @@
ProjectAccessInput,
GitRef,
UrlEncodedRepoName,
- ProjectAccessGroups,
+ RepoAccessGroups,
} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrAccessSection} from '../gr-access-section/gr-access-section';
@@ -79,7 +79,7 @@
@state() capabilities?: CapabilityInfoMap;
// private but used in test
- @state() groups?: ProjectAccessGroups;
+ @state() groups?: RepoAccessGroups;
// private but used in test
@state() inheritsFrom?: ProjectInfo;
@@ -318,7 +318,7 @@
this.editing = false;
- // Always reset sections when a project changes.
+ // Always reset sections when a repo changes.
this.sections = [];
const sectionsPromises = this.restApiService
.getRepoAccessRights(repo, errFn)
@@ -399,17 +399,17 @@
return this.restApiService
.getRepos(this.inheritFromFilter, MAX_AUTOCOMPLETE_RESULTS)
.then(response => {
- const projects: AutocompleteSuggestion[] = [];
+ const repos: AutocompleteSuggestion[] = [];
if (!response) {
- return projects;
+ return repos;
}
for (const item of response) {
- projects.push({
+ repos.push({
name: item.name,
value: item.id,
});
}
- return projects;
+ return repos;
});
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
index 85d5c21..2204400 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
@@ -303,7 +303,7 @@
};
await element.updateComplete;
- // When there is a parent project, the link should be displayed.
+ // When there is a parent repo, the link should be displayed.
assert.notEqual(
getComputedStyle(
queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom')
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 9867aa5..2a648b1 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
@@ -17,7 +17,6 @@
RevisionPatchSetNum,
RepoName,
} from '../../../types/common';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrCreateChangeDialog} from '../gr-create-change-dialog/gr-create-change-dialog';
import {
fireAlert,
@@ -35,6 +34,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {createEditUrl} from '../../../models/views/edit';
import {resolve} from '../../../models/dependency';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const GC_MESSAGE = 'Garbage collection completed successfully.';
const CONFIG_BRANCH = 'refs/meta/config' as BranchName;
@@ -52,8 +52,8 @@
@customElement('gr-repo-commands')
export class GrRepoCommands extends LitElement {
- @query('#createChangeOverlay')
- private readonly createChangeOverlay?: GrOverlay;
+ @query('#createChangeModal')
+ private readonly createChangeModal?: HTMLDialogElement;
@query('#createNewChangeModal')
private readonly createNewChangeModal?: GrCreateChangeDialog;
@@ -88,6 +88,7 @@
formStyles,
subpageStyles,
sharedStyles,
+ modalStyles,
css`
#form h2,
h3 {
@@ -156,7 +157,7 @@
</div>
</div>
</div>
- <gr-overlay id="createChangeOverlay" with-backdrop>
+ <dialog id="createChangeModal" tabindex="-1">
<gr-dialog
id="createChangeDialog"
confirm-label="Create"
@@ -180,7 +181,7 @@
></gr-create-change-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -242,8 +243,8 @@
// private but used in test
createNewChange() {
- assertIsDefined(this.createChangeOverlay, 'createChangeOverlay');
- this.createChangeOverlay.open();
+ assertIsDefined(this.createChangeModal, 'createChangeModal');
+ this.createChangeModal.showModal();
}
// private but used in test
@@ -258,8 +259,8 @@
// private but used in test
handleCloseCreateChange() {
- assertIsDefined(this.createChangeOverlay, 'createChangeOverlay');
- this.createChangeOverlay.close();
+ assertIsDefined(this.createChangeModal, 'createChangeModal');
+ this.createChangeModal.close();
}
/**
@@ -291,7 +292,7 @@
this.getNavigation().setUrl(
createEditUrl({
changeNum: change._number,
- project: change.project,
+ repo: change.project,
path: CONFIG_PATH,
patchNum: INITIAL_PATCHSET,
})
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
index 77caf5e..01140c6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
@@ -12,7 +12,6 @@
queryAndAssert,
stubRestApi,
} from '../../../test/test-utils';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {EventType, PageErrorEvent} from '../../../types/events';
import {RepoName} from '../../../types/common';
@@ -81,13 +80,7 @@
</div>
</div>
</div>
- <gr-overlay
- aria-hidden="true"
- id="createChangeOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="createChangeModal" tabindex="-1">
<gr-dialog
confirm-label="Create"
disabled=""
@@ -100,7 +93,7 @@
</gr-create-change-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`,
{ignoreTags: ['p']}
);
@@ -109,8 +102,8 @@
suite('create new change dialog', () => {
test('createNewChange opens modal', () => {
const openStub = sinon.stub(
- queryAndAssert<GrOverlay>(element, '#createChangeOverlay'),
- 'open'
+ queryAndAssert<HTMLDialogElement>(element, '#createChangeModal'),
+ 'showModal'
);
element.createNewChange();
assert.isTrue(openStub.called);
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 86d4bc5..3b193a6 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
@@ -9,11 +9,9 @@
import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-list-view/gr-list-view';
-import '../../shared/gr-overlay/gr-overlay';
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 {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrCreatePointerDialog} from '../gr-create-pointer-dialog/gr-create-pointer-dialog';
import {
BranchInfo,
@@ -37,14 +35,15 @@
import {assertIsDefined} from '../../../utils/common-util';
import {ifDefined} from 'lit/directives/if-defined.js';
import {RepoDetailView, RepoViewState} from '../../../models/views/repo';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const PGP_START = '-----BEGIN PGP SIGNATURE-----';
@customElement('gr-repo-detail-list')
export class GrRepoDetailList extends LitElement {
- @query('#overlay') private readonly overlay?: GrOverlay;
+ @query('#modal') private readonly modal?: HTMLDialogElement;
- @query('#createOverlay') private readonly createOverlay?: GrOverlay;
+ @query('#createModal') private readonly createModal?: HTMLDialogElement;
@query('#createNewModal')
private readonly createNewModal?: GrCreatePointerDialog;
@@ -91,6 +90,7 @@
formStyles,
tableStyles,
sharedStyles,
+ modalStyles,
css`
.tags td.name {
min-width: 25em;
@@ -189,7 +189,7 @@
.map((item, index) => this.renderItemList(item, index))}
</tbody>
</table>
- <gr-overlay id="overlay" with-backdrop>
+ <dialog id="modal" tabindex="-1">
<gr-confirm-delete-item-dialog
class="confirmDialog"
.item=${this.refName}
@@ -199,9 +199,9 @@
this.handleConfirmDialogCancel();
}}
></gr-confirm-delete-item-dialog>
- </gr-overlay>
+ </dialog>
</gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
id="createDialog"
?disabled=${!this.newItemName}
@@ -228,7 +228,7 @@
></gr-create-pointer-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -531,8 +531,8 @@
}
private handleDeleteItemConfirm() {
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.close();
+ assertIsDefined(this.modal, 'modal');
+ this.modal.close();
if (!this.repo || !this.refName) {
return Promise.reject(new Error('undefined repo or refName'));
}
@@ -569,20 +569,20 @@
}
private handleConfirmDialogCancel() {
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.close();
+ assertIsDefined(this.modal, 'modal');
+ this.modal.close();
}
private handleDeleteItem(index: number) {
if (!this.items) return;
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.modal, 'modal');
const name = this.stripRefs(
this.items[index].ref,
this.detailType
) as GitRef;
if (!name) return;
this.refName = name;
- this.overlay.open();
+ this.modal.showModal();
}
// private but used in test
@@ -594,14 +594,14 @@
// private but used in test
handleCloseCreate() {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.close();
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.close();
}
// private but used in test
handleCreateClicked() {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.open();
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.showModal();
}
private handleUpdateItemName() {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
index 13f6b2b..28fc751 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
@@ -20,8 +20,8 @@
GitRef,
GroupId,
GroupName,
- ProjectAccessGroups,
- ProjectAccessInfoMap,
+ RepoAccessGroups,
+ RepoAccessInfoMap,
RepoName,
TagInfo,
Timestamp,
@@ -30,7 +30,6 @@
import {GerritView} from '../../../services/router/router-model';
import {GrButton} from '../../shared/gr-button/gr-button';
import {PageErrorEvent} from '../../../types/events';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrListView} from '../../shared/gr-list-view/gr-list-view';
import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
@@ -2026,24 +2025,12 @@
</tr>
</tbody>
</table>
- <gr-overlay
- aria-hidden="true"
- id="overlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="modal" tabindex="-1">
<gr-confirm-delete-item-dialog class="confirmDialog">
</gr-confirm-delete-item-dialog>
- </gr-overlay>
+ </dialog>
</gr-list-view>
- <gr-overlay
- aria-hidden="true"
- id="createOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
confirm-label="Create"
disabled=""
@@ -2056,7 +2043,7 @@
</gr-create-pointer-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -2103,10 +2090,10 @@
url: 'test',
name: 'test' as GroupName,
},
- } as ProjectAccessGroups,
+ } as RepoAccessGroups,
config_web_links: [{name: 'gitiles', url: 'test'}],
},
- } as ProjectAccessInfoMap)
+ } as RepoAccessInfoMap)
);
await element.determineIfOwner('test' as RepoName);
assert.equal(element.isOwner, false);
@@ -2157,10 +2144,10 @@
url: 'test',
name: 'test' as GroupName,
},
- } as ProjectAccessGroups,
+ } as RepoAccessGroups,
config_web_links: [{name: 'gitiles', url: 'test'}],
},
- } as ProjectAccessInfoMap)
+ } as RepoAccessInfoMap)
);
const handleSaveRevisionStub = sinon.stub(
element,
@@ -2462,10 +2449,10 @@
});
test('handleCreateClicked opens modal', () => {
- queryAndAssert<GrOverlay>(element, '#createOverlay');
+ queryAndAssert<HTMLDialogElement>(element, '#createModal');
const openStub = sinon.stub(
- queryAndAssert<GrOverlay>(element, '#createOverlay'),
- 'open'
+ queryAndAssert<HTMLDialogElement>(element, '#createModal'),
+ 'showModal'
);
element.handleCreateClicked();
assert.isTrue(openStub.called);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index f0bd520..c6c6efd 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -5,16 +5,14 @@
*/
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-list-view/gr-list-view';
-import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {
RepoName,
ProjectInfoWithName,
WebLinkInfo,
} from '../../../types/common';
import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {RepoState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
@@ -24,6 +22,7 @@
import {customElement, property, query, state} from 'lit/decorators.js';
import {AdminViewState} from '../../../models/views/admin';
import {createSearchUrl} from '../../../models/views/search';
+import {modalStyles} from '../../../styles/gr-modal-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -35,7 +34,7 @@
export class GrRepoList extends LitElement {
readonly path = '/admin/repos';
- @query('#createOverlay') private createOverlay?: GrOverlay;
+ @query('#createModal') private createModal?: HTMLDialogElement;
@query('#createNewModal') private createNewModal?: GrCreateRepoDialog;
@@ -67,13 +66,14 @@
super.connectedCallback();
await this.getCreateRepoCapability();
fireTitleChange(this, 'Repos');
- this.maybeOpenCreateOverlay(this.params);
+ this.maybeOpenCreateModal(this.params);
}
static override get styles() {
return [
tableStyles,
sharedStyles,
+ modalStyles,
css`
.genericList tr td:last-of-type {
text-align: left;
@@ -127,7 +127,7 @@
</tbody>
</table>
</gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
id="createDialog"
class="confirmDialog"
@@ -144,7 +144,7 @@
></gr-create-repo-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -164,7 +164,7 @@
<a href=${this.computeChangesLink(item.name)}>view all</a>
</td>
<td class="readOnly">
- ${item.state === ProjectState.READ_ONLY ? 'Y' : ''}
+ ${item.state === RepoState.READ_ONLY ? 'Y' : ''}
</td>
<td class="description">${item.description}</td>
</tr>
@@ -204,9 +204,9 @@
*
* private but used in test
*/
- maybeOpenCreateOverlay(params?: AdminViewState) {
+ maybeOpenCreateModal(params?: AdminViewState) {
if (params?.openCreateModal) {
- this.createOverlay?.open();
+ this.createModal?.showModal();
}
}
@@ -215,7 +215,7 @@
}
private computeChangesLink(name: string) {
- return createSearchUrl({project: name as RepoName});
+ return createSearchUrl({repo: name as RepoName});
}
private async getCreateRepoCapability() {
@@ -270,14 +270,13 @@
// private but used in test
handleCloseCreate() {
- this.createOverlay?.close();
+ this.createModal?.close();
}
// private but used in test
handleCreateClicked() {
- this.createOverlay?.open().then(() => {
- this.createNewModal?.focus();
- });
+ this.createModal?.showModal();
+ this.createNewModal?.focus();
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
index 29d378d..5b65942 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
@@ -17,9 +17,8 @@
ProjectInfoWithName,
RepoName,
} from '../../../types/common';
-import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {RepoState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {GerritView} from '../../../services/router/router-model';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrListView} from '../../shared/gr-list-view/gr-list-view';
import {fixture, html, assert} from '@open-wc/testing';
@@ -29,7 +28,7 @@
return {
id: `${name}${counter}` as UrlEncodedRepoName,
name: `${name}` as RepoName,
- state: 'ACTIVE' as ProjectState,
+ state: 'ACTIVE' as RepoState,
web_links: [
{
name: 'diffusion',
@@ -588,13 +587,7 @@
</tbody>
</table>
</gr-list-view>
- <gr-overlay
- aria-hidden="true"
- id="createOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
class="confirmDialog"
confirm-label="Create"
@@ -608,7 +601,7 @@
</gr-create-repo-dialog>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -624,22 +617,22 @@
assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
});
- test('maybeOpenCreateOverlay', () => {
- const overlayOpen = sinon.stub(
- queryAndAssert<GrOverlay>(element, '#createOverlay'),
- 'open'
+ test('maybeOpenCreateModal', () => {
+ const modalOpen = sinon.stub(
+ queryAndAssert<HTMLDialogElement>(element, '#createModal'),
+ 'showModal'
);
- element.maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- element.maybeOpenCreateOverlay(undefined);
- assert.isFalse(overlayOpen.called);
+ element.maybeOpenCreateModal();
+ assert.isFalse(modalOpen.called);
+ element.maybeOpenCreateModal(undefined);
+ assert.isFalse(modalOpen.called);
const params: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.REPOS,
openCreateModal: true,
};
- element.maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
+ element.maybeOpenCreateModal(params);
+ assert.isTrue(modalOpen.called);
});
});
@@ -749,9 +742,10 @@
});
test('handleCreateClicked opens modal', () => {
- const openStub = sinon
- .stub(queryAndAssert<GrOverlay>(element, '#createOverlay'), 'open')
- .returns(Promise.resolve());
+ const openStub = sinon.stub(
+ queryAndAssert<HTMLDialogElement>(element, '#createModal'),
+ 'showModal'
+ );
element.handleCreateClicked();
assert.isTrue(openStub.called);
});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index 01d452a..8d6d89a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -22,7 +22,7 @@
} from '../../../types/common';
import {
InheritedBooleanInfoConfiguredValue,
- ProjectState,
+ RepoState,
SubmitType,
} from '../../../constants/constants';
import {hasOwnProperty} from '../../../utils/common-util';
@@ -40,11 +40,13 @@
import {customElement, property, state} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
import {createSearchUrl} from '../../../models/views/search';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
const STATES = {
- active: {value: ProjectState.ACTIVE, label: 'Active'},
- readOnly: {value: ProjectState.READ_ONLY, label: 'Read Only'},
- hidden: {value: ProjectState.HIDDEN, label: 'Hidden'},
+ active: {value: RepoState.ACTIVE, label: 'Active'},
+ readOnly: {value: RepoState.READ_ONLY, label: 'Read Only'},
+ hidden: {value: RepoState.HIDDEN, label: 'Hidden'},
};
const SUBMIT_TYPES = {
@@ -110,7 +112,7 @@
@state() private pluginConfigChanged = false;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly restApiService = getAppContext().restApiService;
@@ -118,7 +120,7 @@
super();
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
if (prefs?.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
@@ -1098,7 +1100,7 @@
private computeChangesUrl(name?: RepoName) {
if (!name) return '';
- return createSearchUrl({project: name});
+ return createSearchUrl({repo: name});
}
// private but used in test
@@ -1130,7 +1132,7 @@
if (!this.repoConfig || this.loading) return;
this.repoConfig = {
...this.repoConfig,
- state: e.detail.value as ProjectState,
+ state: e.detail.value as RepoState,
};
this.requestUpdate();
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
index 38c4a5d..c013c9e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
@@ -25,14 +25,14 @@
InheritedBooleanInfo,
MaxObjectSizeLimitInfo,
PluginParameterToConfigParameterInfoMap,
- ProjectAccessGroups,
- ProjectAccessInfoMap,
+ RepoAccessGroups,
+ RepoAccessInfoMap,
RepoName,
} from '../../../types/common';
import {
ConfigParameterInfoType,
InheritedBooleanInfoConfiguredValue,
- ProjectState,
+ RepoState,
SubmitType,
} from '../../../constants/constants';
import {
@@ -52,7 +52,7 @@
let repoStub: sinon.SinonStub;
const repoConf: ConfigInfo = {
- description: 'Access inherited by all other projects.',
+ description: 'Access inherited by all other repositories.',
use_contributor_agreements: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
@@ -534,10 +534,10 @@
url: 'test',
name: 'test' as GroupName,
},
- } as ProjectAccessGroups,
+ } as RepoAccessGroups,
config_web_links: [{name: 'gitiles', url: 'test'}],
},
- } as ProjectAccessInfoMap)
+ } as RepoAccessInfoMap)
);
await element.loadRepo();
assert.isTrue(element.readOnly);
@@ -652,10 +652,10 @@
url: 'test',
name: 'test' as GroupName,
},
- } as ProjectAccessGroups,
+ } as RepoAccessGroups,
config_web_links: [{name: 'gitiles', url: 'test'}],
},
- } as ProjectAccessInfoMap)
+ } as RepoAccessInfoMap)
);
});
@@ -671,10 +671,10 @@
test('state gets set correctly', async () => {
await element.loadRepo();
- assert.equal(element.repoConfig!.state, ProjectState.ACTIVE);
+ assert.equal(element.repoConfig!.state, RepoState.ACTIVE);
assert.equal(
queryAndAssert<GrSelect>(element, '#stateSelect').bindValue,
- ProjectState.ACTIVE
+ RepoState.ACTIVE
);
});
@@ -707,7 +707,7 @@
reject_empty_commit: InheritedBooleanInfoConfiguredValue.TRUE,
max_object_size_limit: '10' as MaxObjectSizeLimitInfo,
submit_type: SubmitType.FAST_FORWARD_ONLY,
- state: ProjectState.READ_ONLY,
+ state: RepoState.READ_ONLY,
enable_reviewer_by_email: InheritedBooleanInfoConfiguredValue.TRUE,
};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index 5728529..a9b8028 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -5,7 +5,6 @@
*/
import {customElement, query, state} from 'lit/decorators.js';
import {LitElement, html, css, nothing} from 'lit';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {resolve} from '../../../models/dependency';
import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
import {subscribe} from '../../lit/subscription-controller';
@@ -39,12 +38,14 @@
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {Interaction} from '../../../constants/reporting';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
+import {modalStyles} from '../../../styles/gr-modal-styles';
@customElement('gr-change-list-bulk-vote-flow')
export class GrChangeListBulkVoteFlow extends LitElement {
private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly reportingService = getAppContext().reportingService;
@@ -52,7 +53,7 @@
@state() progressByChange: Map<NumericChangeId, ProgressStatus> = new Map();
- @query('#actionOverlay') actionOverlay!: GrOverlay;
+ @query('#actionModal') actionModal!: HTMLDialogElement;
@query('gr-dialog') dialog?: GrDialog;
@@ -61,6 +62,7 @@
static override get styles() {
return [
fontStyles,
+ modalStyles,
css`
gr-dialog {
width: 840px;
@@ -141,7 +143,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
account => (this.account = account)
);
}
@@ -153,10 +155,10 @@
permittedLabels
).filter(label => !triggerLabels.some(l => l.name === label.name));
return html`
- <gr-button id="voteFlowButton" flatten @click=${this.openOverlay}
+ <gr-button id="voteFlowButton" flatten @click=${this.openModal}
>Vote</gr-button
>
- <gr-overlay id="actionOverlay" with-backdrop="">
+ <dialog id="actionModal" tabindex="-1">
<gr-dialog
.disableCancel=${!this.isCancelEnabled()}
.disabled=${!this.isConfirmEnabled()}
@@ -185,7 +187,7 @@
${this.renderErrors()}
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -223,12 +225,8 @@
}
}
- private async openOverlay() {
- await this.actionOverlay.open();
- this.actionOverlay.setFocusStops({
- start: queryAndAssert(this.dialog, 'header'),
- end: queryAndAssert(this.dialog, 'footer'),
- });
+ private openModal() {
+ this.actionModal.showModal();
}
private renderErrors() {
@@ -304,7 +302,7 @@
}
private handleClose() {
- this.actionOverlay.close();
+ this.actionModal.close();
if (getOverallStatus(this.progressByChange) === ProgressStatus.NOT_STARTED)
return;
fireReload(this, true);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 0ca3976..71977d1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -147,12 +147,9 @@
>
Vote
</gr-button>
- <gr-overlay
- aria-hidden="true"
- id="actionOverlay"
- style="outline: none; display: none;"
+ <dialog
+ id="actionModal"
tabindex="-1"
- with-backdrop=""
>
<gr-dialog role="dialog">
<div slot="header">
@@ -197,7 +194,7 @@
</div>
</div>
</gr-dialog>
- </gr-overlay> `
+ </dialog> `
);
});
@@ -238,12 +235,9 @@
>
Vote
</gr-button>
- <gr-overlay
- aria-hidden="true"
- id="actionOverlay"
- style="outline: none; display: none;"
+ <dialog
+ id="actionModal"
tabindex="-1"
- with-backdrop=""
>
<gr-dialog role="dialog">
<div slot="header">
@@ -367,10 +361,7 @@
const saveChangeReview = mockPromise<Response>();
stubRestApi('saveChangeReview').returns(saveChangeReview);
- const stopsStub = sinon.stub(element.actionOverlay, 'setFocusStops');
-
queryAndAssert<GrButton>(element, '#voteFlowButton').click();
- await waitUntil(() => stopsStub.called);
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
index 2b3c3df4..ad1f718 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
@@ -15,7 +15,7 @@
import '@polymer/iron-dropdown/iron-dropdown';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {getAppContext} from '../../../services/app-context';
-import {notUndefined} from '../../../types/types';
+import {isDefined} from '../../../types/types';
import {unique} from '../../../utils/common-util';
import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
import {when} from 'lit/directives/when.js';
@@ -215,7 +215,7 @@
private renderExistingHashtags() {
const hashtags = this.selectedChanges
.flatMap(change => change.hashtags ?? [])
- .filter(notUndefined)
+ .filter(isDefined)
.filter(unique);
return html`
<div class="chips">
@@ -302,7 +302,7 @@
);
this.existingHashtagSuggestions = (suggestions ?? [])
.flatMap(change => change.hashtags ?? [])
- .filter(notUndefined)
+ .filter(isDefined)
.filter(unique);
return this.existingHashtagSuggestions.map(hashtag => {
return {name: hashtag, value: hashtag};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 04f282a..d9982b1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -17,13 +17,10 @@
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {getDisplayName} from '../../../utils/display-name-util';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {getAppContext} from '../../../services/app-context';
import {truncatePath} from '../../../utils/path-list-util';
import {changeStatuses} from '../../../utils/change-util';
import {isSelf, isServiceUser} from '../../../utils/account-util';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {
ChangeInfo,
ServerInfo,
@@ -44,6 +41,7 @@
import {classMap} from 'lit/directives/class-map.js';
import {createSearchUrl} from '../../../models/views/search';
import {createChangeUrl} from '../../../models/views/change';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
enum ChangeSize {
XS = 10,
@@ -118,7 +116,9 @@
@state() private dynamicCellEndpoints?: string[];
- reporting: ReportingService = getAppContext().reportingService;
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
@@ -138,12 +138,13 @@
override connectedCallback() {
super.connectedCallback();
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
- this.dynamicCellEndpoints = getPluginEndpoints().getDynamicEndpoints(
- 'change-list-item-cell'
- );
+ this.dynamicCellEndpoints =
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-list-item-cell'
+ );
});
this.addEventListener('click', this.onItemClick);
}
@@ -671,14 +672,14 @@
private computeRepoUrl() {
if (!this.change) return '';
- return createSearchUrl({project: this.change.project, statuses: ['open']});
+ return createSearchUrl({repo: this.change.project, statuses: ['open']});
}
private computeRepoBranchURL() {
if (!this.change) return '';
return createSearchUrl({
branch: this.change.branch,
- project: this.change.project,
+ repo: this.change.project,
});
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index 436e435..1789fcb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -18,16 +18,14 @@
SuggestedReviewerGroupInfo,
} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
-import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icon/gr-icon';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
import {
GrReviewerSuggestionsProvider,
ReviewerSuggestionsProvider,
-} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+} from '../../../services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import '../../shared/gr-account-list/gr-account-list';
import {getOverallStatus} from '../../../utils/bulk-flow-util';
import {allSettled} from '../../../utils/async-util';
@@ -38,12 +36,14 @@
GrAccountList,
} from '../../shared/gr-account-list/gr-account-list';
import {getReplyByReason} from '../../../utils/attention-set-util';
-import {intersection, queryAndAssert} from '../../../utils/common-util';
+import {intersection} from '../../../utils/common-util';
import {accountKey, getUserId} from '../../../utils/account-util';
import {ValueChangedEvent} from '../../../types/events';
import {fireAlert, fireReload} from '../../../utils/event-util';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {Interaction} from '../../../constants/reporting';
+import {userModelToken} from '../../../models/user/user-model';
+import {modalStyles} from '../../../styles/gr-modal-styles';
@customElement('gr-change-list-reviewer-flow')
export class GrChangeListReviewerFlow extends LitElement {
@@ -80,24 +80,26 @@
[ReviewerState.CC, null],
]);
- @query('gr-overlay#flow') private overlay?: GrOverlay;
+ @query('dialog#flow') private modal?: HTMLDialogElement;
@query('gr-account-list#reviewer-list') private reviewerList?: GrAccountList;
@query('gr-account-list#cc-list') private ccList?: GrAccountList;
- @query('gr-overlay#confirm-reviewer')
- private reviewerConfirmOverlay?: GrOverlay;
+ @query('dialog#confirm-reviewer')
+ private reviewerConfirmModal?: HTMLDialogElement;
- @query('gr-overlay#confirm-cc') private ccConfirmOverlay?: GrOverlay;
+ @query('dialog#confirm-cc') private ccConfirmModal?: HTMLDialogElement;
@query('gr-dialog') dialog?: GrDialog;
private readonly reportingService = getAppContext().reportingService;
- private getBulkActionsModel = resolve(this, bulkActionsModelToken);
+ private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
- private getConfigModel = resolve(this, configModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private restApiService = getAppContext().restApiService;
@@ -107,6 +109,7 @@
static override get styles() {
return [
+ modalStyles,
css`
gr-dialog {
width: 60em;
@@ -143,8 +146,8 @@
color: var(--orange-800);
font-size: 18px;
}
- gr-overlay#confirm-cc,
- gr-overlay#confirm-reviewer {
+ dialog#confirm-cc,
+ dialog#confirm-reviewer {
padding: var(--spacing-l);
text-align: center;
}
@@ -169,12 +172,12 @@
);
subscribe(
this,
- () => getAppContext().userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
subscribe(
this,
- () => getAppContext().userModel.account$,
+ () => this.getUserModel().account$,
account => (this.account = account)
);
}
@@ -188,9 +191,9 @@
@click=${() => this.openOverlay()}
>add reviewer/cc</gr-button
>
- <gr-overlay id="flow" with-backdrop>
+ <dialog id="flow" tabindex="-1">
${this.isOverlayOpen ? this.renderDialog() : nothing}
- </gr-overlay>
+ </dialog>
`;
}
@@ -262,7 +265,8 @@
const suggestion =
this.groupPendingConfirmationByReviewerState.get(reviewerState);
return html`
- <gr-overlay
+ <dialog
+ tabindex="-1"
id=${id}
@iron-overlay-canceled=${() => this.cancelPendingGroup(reviewerState)}
>
@@ -284,7 +288,7 @@
>No</gr-button
>
</div>
- </gr-overlay>
+ </dialog>
`;
}
@@ -378,16 +382,12 @@
this.resetFlow();
this.isOverlayOpen = true;
// Must await the overlay opening because the dialog is lazily rendered.
- await this.overlay?.open();
- this.overlay?.setFocusStops({
- start: queryAndAssert(this.dialog, 'header'),
- end: queryAndAssert(this.dialog, 'footer'),
- });
+ await this.modal?.showModal();
}
private closeOverlay() {
this.isOverlayOpen = false;
- this.overlay?.close();
+ this.modal?.close();
}
private resetFlow() {
@@ -454,22 +454,22 @@
this.requestUpdate();
await this.updateComplete;
- const overlay =
+ const modal =
reviewerState === ReviewerState.CC
- ? this.ccConfirmOverlay
- : this.reviewerConfirmOverlay;
+ ? this.ccConfirmModal
+ : this.reviewerConfirmModal;
if (ev.detail.value === null) {
- overlay?.close();
+ modal?.close();
} else {
- await overlay?.open();
+ await modal?.showModal();
}
}
private cancelPendingGroup(reviewerState: ReviewerState) {
const overlay =
reviewerState === ReviewerState.CC
- ? this.ccConfirmOverlay
- : this.reviewerConfirmOverlay;
+ ? this.ccConfirmModal
+ : this.reviewerConfirmModal;
overlay?.close();
this.groupPendingConfirmationByReviewerState.set(reviewerState, null);
this.requestUpdate();
@@ -491,10 +491,10 @@
this.saveReviewers();
break;
case ProgressStatus.SUCCESSFUL:
- this.overlay?.close();
+ this.modal?.close();
break;
case ProgressStatus.FAILED:
- this.overlay?.close();
+ this.modal?.close();
break;
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index a96085c..85e6212 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -40,7 +40,6 @@
import {GrAccountList} from '../../shared/gr-account-list/gr-account-list';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import './gr-change-list-reviewer-flow';
import type {GrChangeListReviewerFlow} from './gr-change-list-reviewer-flow';
@@ -122,13 +121,7 @@
tabindex="0"
>add reviewer/cc</gr-button
>
- <gr-overlay
- id="flow"
- aria-hidden="true"
- with-backdrop=""
- tabindex="-1"
- style="outline: none; display: none;"
- ></gr-overlay>
+ <dialog id="flow" tabindex="-1"></dialog>
`
);
});
@@ -148,18 +141,21 @@
});
test('overlay hidden before flow button clicked', async () => {
- const overlay = queryAndAssert<GrOverlay>(element, 'gr-overlay');
- assert.isFalse(overlay.opened);
+ const dialog = queryAndAssert<HTMLDialogElement>(element, 'dialog');
+ const openStub = sinon.stub(dialog, 'showModal');
+ assert.isFalse(openStub.called);
});
test('flow button click shows overlay', async () => {
const button = queryAndAssert<GrButton>(element, 'gr-button#start-flow');
+ const dialog = queryAndAssert<HTMLDialogElement>(element, 'dialog');
+ const openStub = sinon.stub(dialog, 'showModal');
button.click();
+
await element.updateComplete;
- const overlay = queryAndAssert<GrOverlay>(element, 'gr-overlay');
- assert.isTrue(overlay.opened);
+ assert.isTrue(openStub.called);
});
suite('dialog flow', () => {
@@ -202,23 +198,14 @@
tabindex="0"
>add reviewer/cc</gr-button
>
- <gr-overlay
- id="flow"
- with-backdrop=""
- tabindex="-1"
- style="outline: none; display: none;"
- >
+ <dialog id="flow" open="" tabindex="-1">
<gr-dialog role="dialog">
<div slot="header">Add reviewer / CC</div>
<div slot="main">
<div class="grid">
<span>Reviewers</span>
<gr-account-list id="reviewer-list"></gr-account-list>
- <gr-overlay
- aria-hidden="true"
- id="confirm-reviewer"
- style="outline: none; display: none;"
- >
+ <dialog id="confirm-reviewer" tabindex="-1">
<div class="confirmation-text">
Group
<span class="groupName"></span>
@@ -244,14 +231,10 @@
No
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
<span>CC</span>
<gr-account-list id="cc-list"></gr-account-list>
- <gr-overlay
- aria-hidden="true"
- id="confirm-cc"
- style="outline: none; display: none;"
- >
+ <dialog id="confirm-cc" tabindex="-1">
<div class="confirmation-text">
Group
<span class="groupName"></span>
@@ -277,11 +260,12 @@
No
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
</div>
</div>
</gr-dialog>
- </gr-overlay>
+ <div id="gr-hovercard-container"></div>
+ </dialog>
`
);
});
@@ -645,14 +629,14 @@
tabindex="0"
>add reviewer/cc</gr-button
>
- <gr-overlay id="flow" with-backdrop="" tabindex="-1">
+ <dialog id="flow" open="" tabindex="-1">
<gr-dialog role="dialog">
<div slot="header">Add reviewer / CC</div>
<div slot="main">
<div class="grid">
<span>Reviewers</span>
<gr-account-list id="reviewer-list"></gr-account-list>
- <gr-overlay aria-hidden="true" id="confirm-reviewer">
+ <dialog tabindex="-1" id="confirm-reviewer">
<div class="confirmation-text">
Group
<span class="groupName"></span>
@@ -676,10 +660,10 @@
No
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
<span>CC</span>
<gr-account-list id="cc-list"></gr-account-list>
- <gr-overlay aria-hidden="true" id="confirm-cc">
+ <dialog tabindex="-1" id="confirm-cc">
<div class="confirmation-text">
Group
<span class="groupName"></span>
@@ -703,7 +687,7 @@
No
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
</div>
<div class="warning">
<gr-icon icon="warning" filled role="img" aria-label="Warning"
@@ -721,11 +705,13 @@
</div>
</div>
</gr-dialog>
- </gr-overlay>
+ <div id="gr-hovercard-container">
+ </div>
+ </dialog>
`,
{
- // gr-overlay sizing seems to vary between local & CI
- ignoreAttributes: [{tags: ['gr-overlay'], attributes: ['style']}],
+ // dialog sizing seems to vary between local & CI
+ ignoreAttributes: [{tags: ['dialog'], attributes: ['style']}],
}
);
});
@@ -759,10 +745,10 @@
>
add reviewer/cc
</gr-button>
- <gr-overlay
+ <dialog
id="flow"
tabindex="-1"
- with-backdrop=""
+ open=""
>
<gr-dialog role="dialog">
<div slot="header">Add reviewer / CC</div>
@@ -770,7 +756,7 @@
<div class="grid">
<span> Reviewers </span>
<gr-account-list id="reviewer-list"> </gr-account-list>
- <gr-overlay aria-hidden="true" id="confirm-reviewer">
+ <dialog tabindex="-1" id="confirm-reviewer">
<div class="confirmation-text">
Group
<span class="groupName"> </span>
@@ -796,10 +782,10 @@
No
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
<span> CC </span>
<gr-account-list id="cc-list"> </gr-account-list>
- <gr-overlay aria-hidden="true" id="confirm-cc">
+ <dialog tabindex="-1" id="confirm-cc">
<div class="confirmation-text">
Group
<span class="groupName"> </span>
@@ -825,7 +811,7 @@
No
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
</div>
<div class="error">
<gr-icon icon="error" filled role="img" aria-label="Error"></gr-icon>
@@ -833,11 +819,13 @@
</div>
</div>
</gr-dialog>
- </gr-overlay>
+ <div id="gr-hovercard-container">
+ </div>
+ </dialog>
`,
{
- // gr-overlay sizing seems to vary between local & CI
- ignoreAttributes: [{tags: ['gr-overlay'], attributes: ['style']}],
+ // dialog sizing seems to vary between local & CI
+ ignoreAttributes: [{tags: ['dialog'], attributes: ['style']}],
}
);
});
@@ -866,10 +854,7 @@
await reviewerList.updateComplete;
await element.updateComplete;
- const confirmDialog = queryAndAssert(
- element,
- 'gr-overlay#confirm-reviewer'
- );
+ const confirmDialog = queryAndAssert(element, 'dialog#confirm-reviewer');
await waitUntil(
() =>
getComputedStyle(confirmDialog).getPropertyValue('display') !== 'none'
@@ -906,12 +891,10 @@
).click();
await element.updateComplete;
- const confirmDialog = queryAndAssert(
- element,
- 'gr-overlay#confirm-reviewer'
- );
- assert.isTrue(
- getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
+ const confirmDialog = queryAndAssert(element, 'dialog#confirm-reviewer');
+ await waitUntil(
+ () =>
+ getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
);
assert.deepEqual(reviewerList.accounts[1], {
@@ -947,10 +930,7 @@
// triggers an update of ReviewerFlow
await reviewerList.updateComplete;
await element.updateComplete;
- const confirmDialog = queryAndAssert(
- element,
- 'gr-overlay#confirm-reviewer'
- );
+ const confirmDialog = queryAndAssert(element, 'dialog#confirm-reviewer');
assert.isTrue(
getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
);
@@ -990,10 +970,7 @@
).click();
await element.updateComplete;
- const confirmDialog = queryAndAssert(
- element,
- 'gr-overlay#confirm-reviewer'
- );
+ const confirmDialog = queryAndAssert(element, 'dialog#confirm-reviewer');
assert.isTrue(
getComputedStyle(confirmDialog).getPropertyValue('display') === 'none'
);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
index ac1ba23..363b1ae 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
@@ -15,7 +15,7 @@
import '@polymer/iron-dropdown/iron-dropdown';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {getAppContext} from '../../../services/app-context';
-import {notUndefined} from '../../../types/types';
+import {isDefined} from '../../../types/types';
import {unique} from '../../../utils/common-util';
import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
import {when} from 'lit/directives/when.js';
@@ -190,7 +190,7 @@
private renderExistingTopicsMode() {
const topics = this.selectedChanges
.map(change => change.topic)
- .filter(notUndefined)
+ .filter(isDefined)
.filter(unique);
const removeDisabled =
this.selectedExistingTopics.size === 0 ||
@@ -347,7 +347,7 @@
);
this.existingTopicSuggestions = (suggestions ?? [])
.map(change => change.topic)
- .filter(notUndefined)
+ .filter(isDefined)
.filter(unique);
return this.existingTopicSuggestions.map(topic => {
return {name: topic, value: topic};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index a58c7bb..cd5cb96 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -7,7 +7,6 @@
import '../gr-repo-header/gr-repo-header';
import '../gr-user-header/gr-user-header';
import {page} from '../../../utils/page-wrapper-utils';
-import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
AccountDetailInfo,
AccountId,
@@ -22,29 +21,13 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css, nothing} from 'lit';
import {customElement, state, query} from 'lit/decorators.js';
-import {ValueChangedEvent} from '../../../types/events';
import {
createSearchUrl,
searchViewModelToken,
- SearchViewState,
} from '../../../models/views/search';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
-import {createChangeUrl} from '../../../models/views/change';
-import {debounce, DelayedTask} from '../../../utils/async-util';
-
-const GET_CHANGES_DEBOUNCE_INTERVAL_MS = 10;
-
-const LOOKUP_QUERY_PATTERNS: RegExp[] = [
- /^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
- /^\s*[1-9][0-9]*\s*$/g, // CHANGE_NUM
- /[0-9a-f]{40}/, // COMMIT
-];
-
-const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
-
-const REPO_QUERY_PATTERN =
- /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+import {userModelToken} from '../../../models/user/user-model';
const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
@@ -60,21 +43,6 @@
@query('#nextArrow') protected nextArrow?: HTMLAnchorElement;
- private _viewState?: SearchViewState;
-
- @state()
- get viewState() {
- return this._viewState;
- }
-
- set viewState(viewState: SearchViewState | undefined) {
- if (this._viewState === viewState) return;
- const oldViewState = this._viewState;
- this._viewState = viewState;
- this.viewStateChanged();
- this.requestUpdate('viewState', oldViewState);
- }
-
// private but used in test
@state() account?: AccountDetailInfo;
@@ -91,68 +59,83 @@
@state() query = '';
// private but used in test
- @state() offset?: number;
+ @state() offset = 0;
// private but used in test
- @state() changes?: ChangeInfo[];
+ @state() changes: ChangeInfo[] = [];
// private but used in test
@state() loading = true;
// private but used in test
- @state() userId: AccountId | EmailAddress | null = null;
+ @state() userId?: AccountId | EmailAddress;
// private but used in test
- @state() repo: RepoName | null = null;
-
- @state() selectedIndex = 0;
+ @state() repo?: RepoName;
private readonly restApiService = getAppContext().restApiService;
private reporting = getAppContext().reportingService;
- private userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getViewModel = resolve(this, searchViewModelToken);
- private readonly getNavigation = resolve(this, navigationToken);
-
constructor() {
super();
this.addEventListener('next-page', () => this.handleNextPage());
this.addEventListener('previous-page', () => this.handlePreviousPage());
- this.addEventListener('reload', () => this.reload());
+
subscribe(
this,
- () => this.getViewModel().state$,
- x => (this.viewState = x)
+ () => this.getViewModel().query$,
+ x => (this.query = x)
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getViewModel().offsetNumber$,
+ x => (this.offset = x)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().loading$,
+ x => (this.loading = x)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().changes$,
+ x => (this.changes = x)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().userId$,
+ x => (this.userId = x)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().repo$,
+ x => (this.repo = x)
+ );
+ subscribe(
+ this,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
x => (this.loggedIn = x)
);
subscribe(
this,
- () => this.userModel.preferences$,
- x => {
- this.preferences = x;
- if (this.changesPerPage !== x.changes_per_page) {
- this.changesPerPage = x.changes_per_page;
- this.debouncedGetChanges();
- }
- }
+ () => this.getUserModel().preferenceChangesPerPage$,
+ x => (this.changesPerPage = x)
);
- }
-
- override disconnectedCallback() {
- this.getChangesTask?.flush();
- super.disconnectedCallback();
+ subscribe(
+ this,
+ () => this.getUserModel().preferences$,
+ x => (this.preferences = x)
+ );
}
static override get styles() {
@@ -211,10 +194,6 @@
.changes=${this.changes}
.preferences=${this.preferences}
.showStar=${this.loggedIn}
- .selectedIndex=${this.selectedIndex}
- @selected-index-changed=${(e: ValueChangedEvent<number>) => {
- this.selectedIndex = e.detail.value;
- }}
@toggle-star=${(e: CustomEvent<ChangeStarToggleStarDetail>) => {
this.handleToggleStar(e);
}}
@@ -276,71 +255,12 @@
`;
}
- override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('changes')) {
- this.changesChanged();
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('query')) {
+ fireTitleChange(this, this.query);
}
}
- reload() {
- if (!this.loading) this.debouncedGetChanges();
- }
-
- // private, but visible for testing
- viewStateChanged() {
- if (!this.viewState) return;
-
- let offset = Number(this.viewState.offset);
- if (isNaN(offset)) offset = 0;
- const query = this.viewState.query ?? '';
-
- if (this.query !== query) this.selectedIndex = 0;
- this.loading = true;
- this.query = query;
- this.offset = offset;
-
- // NOTE: This method may be called before attachment. Fire title-change
- // in an async so that attachment to the DOM can take place first.
- setTimeout(() => fireTitleChange(this, this.query));
-
- this.debouncedGetChanges(true);
- }
-
- private getChangesTask?: DelayedTask;
-
- private debouncedGetChanges(shouldSingleMatchRedirect = false) {
- this.getChangesTask = debounce(
- this.getChangesTask,
- () => {
- this.getChanges(shouldSingleMatchRedirect);
- },
- GET_CHANGES_DEBOUNCE_INTERVAL_MS
- );
- }
-
- async getChanges(shouldSingleMatchRedirect = false) {
- this.loading = true;
- const changes =
- (await this.restApiService.getChanges(
- this.changesPerPage,
- this.query,
- this.offset
- )) ?? [];
- if (shouldSingleMatchRedirect && this.query && changes.length === 1) {
- for (const queryPattern of LOOKUP_QUERY_PATTERNS) {
- if (this.query.match(queryPattern)) {
- // "Back"/"Forward" buttons work correctly only with replaceUrl()
- this.getNavigation().replaceUrl(
- createChangeUrl({change: changes[0]})
- );
- return;
- }
- }
- }
- this.changes = changes;
- this.loading = false;
- }
-
// private but used in test
limitFor(query: string, defaultLimit?: number) {
if (defaultLimit === undefined) return 0;
@@ -371,26 +291,6 @@
page.show(this.computeNavLink(-1));
}
- private changesChanged() {
- this.userId = null;
- this.repo = null;
- const changes = this.changes;
- if (!changes || !changes.length) {
- return;
- }
- if (USER_QUERY_PATTERN.test(this.query)) {
- const owner = changes[0].owner;
- const userId = owner._account_id ? owner._account_id : owner.email;
- if (userId) {
- this.userId = userId;
- return;
- }
- }
- if (REPO_QUERY_PATTERN.test(this.query)) {
- this.repo = changes[0].project;
- }
- }
-
// private but used in test
computePage() {
if (this.offset === undefined || this.changesPerPage === undefined) return;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
index b003b66..399631e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -7,45 +7,20 @@
import './gr-change-list-view';
import {GrChangeListView} from './gr-change-list-view';
import {page} from '../../../utils/page-wrapper-utils';
-import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {
- query,
- stubRestApi,
- queryAndAssert,
- stubFlags,
-} from '../../../test/test-utils';
+import {query, queryAndAssert} from '../../../test/test-utils';
import {createChange} from '../../../test/test-data-generators';
-import {
- ChangeInfo,
- EmailAddress,
- NumericChangeId,
- RepoName,
-} from '../../../api/rest-api';
+import {ChangeInfo} from '../../../api/rest-api';
import {fixture, html, waitUntil, assert} from '@open-wc/testing';
-import {GerritView} from '../../../services/router/router-model';
-import {testResolver} from '../../../test/common-test-setup';
-import {SinonFakeTimers, SinonStub} from 'sinon';
import {GrChangeList} from '../gr-change-list/gr-change-list';
import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
-const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
-const COMMIT_HASH = '12345678';
-
suite('gr-change-list-view tests', () => {
let element: GrChangeListView;
- let changes: ChangeInfo[] | undefined = [];
- let clock: SinonFakeTimers;
setup(async () => {
- clock = sinon.useFakeTimers();
- stubRestApi('getChanges').callsFake(() => Promise.resolve(changes));
element = await fixture(html`<gr-change-list-view></gr-change-list-view>`);
- element.viewState = {
- view: GerritView.SEARCH,
- query: 'test-query',
- offset: '0',
- };
+ element.query = 'test-query';
await element.updateComplete;
});
@@ -75,11 +50,9 @@
suite('bulk actions', () => {
setup(async () => {
- stubFlags('isEnabled').returns(true);
- changes = [createChange()];
element.loading = false;
- element.reload();
- clock.tick(100);
+ element.changes = [createChange()];
+ await element.updateComplete;
await element.updateComplete;
await waitUntil(() => element.loading === false);
});
@@ -107,8 +80,9 @@
checkbox.click();
await waitUntil(() => checkbox.checked);
- element.reload();
+ element.changes = [createChange()];
await element.updateComplete;
+
checkbox = queryAndAssert<HTMLInputElement>(
query(
query(query(element, 'gr-change-list'), 'gr-change-list-section'),
@@ -220,139 +194,4 @@
element.handlePreviousPage();
assert.isTrue(showStub.called);
});
-
- test('userId query', async () => {
- assert.isNull(element.userId);
- element.query = 'owner: foo@bar';
- element.changes = [
- {...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
- ];
- await element.updateComplete;
- assert.equal(element.userId, 'foo@bar' as EmailAddress);
-
- element.query = 'foo bar baz';
- element.changes = [
- {...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
- ];
- await element.updateComplete;
- assert.isNull(element.userId);
- });
-
- test('userId query without email', async () => {
- assert.isNull(element.userId);
- element.query = 'owner: foo@bar';
- element.changes = [{...createChange(), owner: {}}];
- await element.updateComplete;
- assert.isNull(element.userId);
- });
-
- test('repo query', async () => {
- assert.isNull(element.repo);
- element.query = 'project: test-repo';
- element.changes = [
- {
- ...createChange(),
- owner: {email: 'foo@bar' as EmailAddress},
- project: 'test-repo' as RepoName,
- },
- ];
- await element.updateComplete;
- assert.equal(element.repo, 'test-repo' as RepoName);
-
- element.query = 'foo bar baz';
- element.changes = [
- {...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
- ];
- await element.updateComplete;
- assert.isNull(element.repo);
- });
-
- test('repo query with open status', async () => {
- assert.isNull(element.repo);
- element.query = 'project:test-repo status:open';
- element.changes = [
- {
- ...createChange(),
- owner: {email: 'foo@bar' as EmailAddress},
- project: 'test-repo' as RepoName,
- },
- ];
- await element.updateComplete;
- assert.equal(element.repo, 'test-repo' as RepoName);
-
- element.query = 'foo bar baz';
- element.changes = [
- {...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
- ];
- await element.updateComplete;
- assert.isNull(element.repo);
- });
-
- suite('query based navigation', () => {
- let replaceUrlStub: SinonStub;
- setup(() => {
- replaceUrlStub = sinon.stub(testResolver(navigationToken), 'replaceUrl');
- });
-
- teardown(async () => {
- await element.updateComplete;
- sinon.restore();
- });
-
- test('Searching for a change ID redirects to change', async () => {
- const change = {...createChange(), _number: 1 as NumericChangeId};
- changes = [change];
-
- element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
- clock.tick(100);
- await element.updateComplete;
-
- assert.isTrue(replaceUrlStub.called);
- assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
- });
-
- test('Searching for a change num redirects to change', async () => {
- const change = {...createChange(), _number: 1 as NumericChangeId};
- changes = [change];
-
- element.viewState = {view: GerritView.SEARCH, query: '1'};
- clock.tick(100);
- await element.updateComplete;
-
- assert.isTrue(replaceUrlStub.called);
- assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
- });
-
- test('Commit hash redirects to change', async () => {
- const change = {...createChange(), _number: 1 as NumericChangeId};
- changes = [change];
-
- element.viewState = {view: GerritView.SEARCH, query: COMMIT_HASH};
- clock.tick(100);
- await element.updateComplete;
-
- assert.isTrue(replaceUrlStub.called);
- assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
- });
-
- test('Searching for an invalid change ID searches', async () => {
- changes = [];
-
- element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
- clock.tick(100);
- await element.updateComplete;
-
- assert.isFalse(replaceUrlStub.called);
- });
-
- test('Change ID with multiple search results searches', async () => {
- changes = undefined;
-
- element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
- clock.tick(100);
- await element.updateComplete;
-
- assert.isFalse(replaceUrlStub.called);
- });
- });
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 1eebf88..4c43da5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -10,8 +10,6 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {getAppContext} from '../../../services/app-context';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {
AccountInfo,
@@ -36,6 +34,7 @@
import {ValueChangedEvent} from '../../../types/events';
import {resolve} from '../../../models/dependency';
import {createChangeUrl} from '../../../models/views/change';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
export interface ChangeListSection {
countLabel?: string;
@@ -108,7 +107,7 @@
@state() private dynamicHeaderEndpoints?: string[];
- @property({type: Number}) selectedIndex?: number;
+ @property({type: Number}) selectedIndex = 0;
@property({type: Boolean})
showNumber?: boolean; // No default value to prevent flickering.
@@ -145,6 +144,8 @@
private readonly shortcuts = new ShortcutController(this);
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
private readonly getNavigation = resolve(this, navigationToken);
private cursor = new GrCursorManager();
@@ -179,11 +180,13 @@
this.restApiService.getConfig().then(config => {
this.config = config;
});
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
this.dynamicHeaderEndpoints =
- getPluginEndpoints().getDynamicEndpoints('change-list-header');
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-list-header'
+ );
});
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
index 567c508..cf5c26d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
@@ -4,12 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-dialog/gr-dialog';
-import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-shell-command/gr-shell-command';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, property, query} from 'lit/decorators.js';
+import {modalStyles} from '../../../styles/gr-modal-styles';
enum Commands {
CREATE = 'git commit',
@@ -25,8 +24,8 @@
@customElement('gr-create-commands-dialog')
export class GrCreateCommandsDialog extends LitElement {
- @query('#commandsOverlay')
- commandsOverlay?: GrOverlay;
+ @query('#commandsModal')
+ commandsModal?: HTMLDialogElement;
@property({type: String})
branch?: string;
@@ -34,6 +33,7 @@
static override get styles() {
return [
sharedStyles,
+ modalStyles,
css`
ol {
list-style: decimal;
@@ -50,13 +50,13 @@
}
override render() {
- return html` <gr-overlay id="commandsOverlay" with-backdrop="">
+ return html` <dialog id="commandsModal" tabindex="-1">
<gr-dialog
id="commandsDialog"
confirm-label="Done"
cancel-label=""
confirm-on-enter=""
- @confirm=${() => this.commandsOverlay?.close()}
+ @confirm=${() => this.commandsModal?.close()}
>
<div class="header" slot="header">Create change commands</div>
<div class="main" slot="main">
@@ -90,10 +90,10 @@
</ol>
</div>
</gr-dialog>
- </gr-overlay>`;
+ </dialog>`;
}
open() {
- this.commandsOverlay?.open();
+ this.commandsModal?.showModal();
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts
index 96ec9eb..3252e3d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts
@@ -27,13 +27,7 @@
assert.shadowDom.equal(
element,
/* prettier-ignore */ /* HTML */ `
- <gr-overlay
- aria-hidden="true"
- id="commandsOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="commandsModal" tabindex="-1">
<gr-dialog
cancel-label=""
confirm-label="Done"
@@ -71,7 +65,7 @@
</ol>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
index 983a0d9..561fffd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
@@ -4,15 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-dialog/gr-dialog';
-import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-repo-branch-picker/gr-repo-branch-picker';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {RepoName, BranchName} from '../../../types/common';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, html} from 'lit';
import {customElement, state, query} from 'lit/decorators.js';
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
+import {modalStyles} from '../../../styles/gr-modal-styles';
export interface CreateDestinationConfirmDetail {
repo?: RepoName;
@@ -28,25 +27,25 @@
* @event confirm
*/
- @query('#createOverlay') private createOverlay?: GrOverlay;
+ @query('#createModal') private createModal?: HTMLDialogElement;
@state() private repo?: RepoName;
@state() private branch?: BranchName;
static override get styles() {
- return [sharedStyles];
+ return [sharedStyles, modalStyles];
}
override render() {
return html`
- <gr-overlay id="createOverlay" with-backdrop>
+ <dialog id="createModal" tabindex="-1">
<gr-dialog
confirm-label="View commands"
@confirm=${this.pickerConfirm}
@cancel=${() => {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.close();
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.close();
}}
?disabled=${!(this.repo && this.branch)}
>
@@ -67,20 +66,20 @@
</p>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
open() {
- assertIsDefined(this.createOverlay, 'createOverlay');
+ assertIsDefined(this.createModal, 'createModal');
this.repo = '' as RepoName;
this.branch = '' as BranchName;
- this.createOverlay.open();
+ this.createModal.showModal();
}
private pickerConfirm = (e: Event) => {
- assertIsDefined(this.createOverlay, 'createOverlay');
- this.createOverlay.close();
+ assertIsDefined(this.createModal, 'createModal');
+ this.createModal.close();
const detail: CreateDestinationConfirmDetail = {
repo: this.repo,
branch: this.branch,
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts
index 44b3183..cb27aae 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts
@@ -21,13 +21,7 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-overlay
- aria-hidden="true"
- id="createOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="createModal" tabindex="-1">
<gr-dialog confirm-label="View commands" disabled="" role="dialog">
<div class="header" slot="header">Create change</div>
<div class="main" slot="main">
@@ -37,7 +31,7 @@
</p>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index d178880..6e0e3a9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -27,7 +27,6 @@
CreateDestinationConfirmDetail,
GrCreateDestinationDialog,
} from '../gr-create-destination-dialog/gr-create-destination-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {
fireAlert,
@@ -57,6 +56,9 @@
UserDashboard,
YOUR_TURN,
} from '../../../utils/dashboard-util';
+import {userModelToken} from '../../../models/user/user-model';
+import {Timing} from '../../../constants/reporting';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
@@ -80,7 +82,8 @@
@query('#destinationDialog')
protected destinationDialog?: GrCreateDestinationDialog;
- @query('#confirmDeleteOverlay') protected confirmDeleteOverlay?: GrOverlay;
+ @query('#confirmDeleteModal')
+ protected confirmDeleteModal?: HTMLDialogElement;
@property({type: Object})
account?: AccountDetailInfo;
@@ -107,19 +110,29 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getViewModel = resolve(this, dashboardViewModelToken);
private lastVisibleTimestampMs = 0;
+ /**
+ * For `DASHBOARD_DISPLAYED` timing we can only rely on the router to have
+ * reset the timer properly when the dashboard loads for the first time.
+ * Later we won't have a guarantee that the timer was just reset. So we will
+ * just reset the timer at the beginning of `reload()`. The dashboard view
+ * is cached anyway, so there is unlikely a lot of time that has passed
+ * initiating the reload and the reload() method being executed.
+ */
+ private firstTimeLoad = true;
+
private readonly shortcuts = new ShortcutController(this);
constructor() {
super();
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
@@ -167,6 +180,7 @@
return [
a11yStyles,
sharedStyles,
+ modalStyles,
css`
:host {
display: block;
@@ -208,7 +222,7 @@
if (!this.viewState) return nothing;
return html`
${this.renderBanner()} ${this.renderContent()}
- <gr-overlay id="confirmDeleteOverlay" with-backdrop>
+ <dialog id="confirmDeleteModal" tabindex="-1">
<gr-dialog
id="confirmDeleteDialog"
confirm-label="Delete"
@@ -216,7 +230,7 @@
this.handleConfirmDelete();
}}
@cancel=${() => {
- this.closeConfirmDeleteOverlay();
+ this.closeConfirmDeleteModal();
}}
>
<div class="header" slot="header">Delete comments</div>
@@ -225,7 +239,7 @@
changes? This action cannot be undone.
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
<gr-create-destination-dialog
id="destinationDialog"
@confirm=${(e: CustomEvent<CreateDestinationConfirmDetail>) => {
@@ -330,8 +344,8 @@
}
// private but used in test
- getProjectDashboard(
- project: RepoName,
+ getRepositoryDashboard(
+ repo: RepoName,
dashboard?: DashboardId
): Promise<UserDashboard | undefined> {
const errFn = (response?: Response | null) => {
@@ -339,7 +353,7 @@
};
assertIsDefined(dashboard, 'project dashboard must have id');
return this.restApiService
- .getDashboard(project, dashboard, errFn)
+ .getDashboard(repo, dashboard, errFn)
.then(response => {
if (!response) {
return;
@@ -352,7 +366,7 @@
name: section.name,
query: (section.query + suffix).replace(
PROJECT_PLACEHOLDER_PATTERN,
- project
+ repo
),
};
}),
@@ -375,11 +389,18 @@
*/
reload() {
if (!this.viewState) return Promise.resolve();
+
+ // See `firstTimeLoad` comment above.
+ if (!this.firstTimeLoad) {
+ this.reporting.time(Timing.DASHBOARD_DISPLAYED);
+ }
+ this.firstTimeLoad = false;
+
this.loading = true;
const {project, dashboard, title, user, sections} = this.viewState;
const dashboardPromise: Promise<UserDashboard | undefined> = project
- ? this.getProjectDashboard(project, dashboard)
+ ? this.getRepositoryDashboard(project, dashboard)
: Promise.resolve(
getUserDashboard(user, sections, title || this.computeTitle(user))
);
@@ -565,8 +586,8 @@
// private but used in test
handleOpenDeleteDialog() {
- assertIsDefined(this.confirmDeleteOverlay, 'confirmDeleteOverlay');
- this.confirmDeleteOverlay.open();
+ assertIsDefined(this.confirmDeleteModal, 'confirmDeleteModal');
+ this.confirmDeleteModal.showModal();
}
// private but used in test
@@ -574,14 +595,14 @@
assertIsDefined(this.confirmDeleteDialog, 'confirmDeleteDialog');
this.confirmDeleteDialog.disabled = true;
return this.restApiService.deleteDraftComments('-is:open').then(() => {
- this.closeConfirmDeleteOverlay();
+ this.closeConfirmDeleteModal();
this.reload();
});
}
- private closeConfirmDeleteOverlay() {
- assertIsDefined(this.confirmDeleteOverlay, 'confirmDeleteOverlay');
- this.confirmDeleteOverlay.close();
+ private closeConfirmDeleteModal() {
+ assertIsDefined(this.confirmDeleteModal, 'confirmDeleteModal');
+ this.confirmDeleteModal.close();
}
private computeDraftsLink() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
index 7889e20..17d7e95 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
@@ -29,7 +29,6 @@
RepoName,
Timestamp,
} from '../../../types/common';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrCreateChangeHelp} from '../gr-create-change-help/gr-create-change-help';
import {PageErrorEvent} from '../../../types/events';
@@ -90,12 +89,9 @@
</div>
</gr-change-list>
</div>
- <gr-overlay
- aria-hidden="true"
- id="confirmDeleteOverlay"
- style="outline: none; display: none;"
+ <dialog
+ id="confirmDeleteModal"
tabindex="-1"
- with-backdrop=""
>
<gr-dialog
confirm-label="Delete"
@@ -108,7 +104,7 @@
changes? This action cannot be undone.
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
<gr-create-destination-dialog id="destinationDialog">
</gr-create-destination-dialog>
<gr-create-commands-dialog id="commandsDialog">
@@ -266,11 +262,14 @@
);
// Open confirmation dialog and tap confirm button.
- await queryAndAssert<GrOverlay>(element, '#confirmDeleteOverlay').open();
- queryAndAssert<GrDialog>(
+ const modal = queryAndAssert<HTMLDialogElement>(
element,
- '#confirmDeleteDialog'
- ).confirmButton!.click();
+ '#confirmDeleteModal'
+ );
+ modal.showModal();
+ const dialog = queryAndAssert<GrDialog>(modal, '#confirmDeleteDialog');
+ await waitUntil(() => !!dialog.confirmButton);
+ dialog.confirmButton!.click();
await element.updateComplete;
assert.isTrue(deleteStub.calledWithExactly('-is:open'));
assert.isTrue(
@@ -397,7 +396,7 @@
],
})
);
- const dashboard = await element.getProjectDashboard(
+ const dashboard = await element.getRepositoryDashboard(
'project' as RepoName,
'' as DashboardId
);
@@ -429,7 +428,7 @@
],
})
);
- const dashboard = await element.getProjectDashboard(
+ const dashboard = await element.getRepositoryDashboard(
'project' as RepoName,
'' as DashboardId
);
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 917b7ca..44c0e0c 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
@@ -18,7 +18,6 @@
import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
import '../../../styles/shared-styles';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {getAppContext} from '../../../services/app-context';
import {CURRENT} from '../../../utils/patch-set-util';
import {
@@ -33,7 +32,7 @@
HttpMethod,
NotifyType,
} from '../../../constants/constants';
-import {EventType as PluginEventType, TargetElement} from '../../../api/plugin';
+import {TargetElement} from '../../../api/plugin';
import {
AccountInfo,
ActionInfo,
@@ -54,7 +53,6 @@
ReviewInput,
} from '../../../types/common';
import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrCreateChangeDialog} from '../../admin/gr-create-change-dialog/gr-create-change-dialog';
import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
@@ -107,6 +105,11 @@
import {rootUrl} from '../../../utils/url-util';
import {createSearchUrl} from '../../../models/views/search';
import {createChangeUrl} from '../../../models/views/change';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {ShowRevisionActionsDetail} from '../../shared/gr-js-api-interface/gr-js-api-types';
+import {whenVisible} from '../../../utils/dom-util';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -350,7 +353,7 @@
@query('#mainContent') mainContent?: Element;
- @query('#overlay') overlay?: GrOverlay;
+ @query('#actionsModal') actionsModal?: HTMLDialogElement;
@query('#confirmRebase') confirmRebase?: GrConfirmRebaseDialog;
@@ -388,13 +391,6 @@
RevisionActions = RevisionActions;
- private readonly reporting = getAppContext().reportingService;
-
- // Accessed in tests
- readonly jsAPI = getAppContext().jsApiService;
-
- private readonly getChangeModel = resolve(this, changeModelToken);
-
@property({type: Object})
change?: ChangeViewChangeInfo;
@@ -540,29 +536,33 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly storage = getAppContext().storageService;
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getStorage = resolve(this, storageServiceToken);
private readonly getNavigation = resolve(this, navigationToken);
constructor() {
super();
- this.addEventListener('fullscreen-overlay-opened', () =>
- this.handleHideBackgroundContent()
- );
- this.addEventListener('fullscreen-overlay-closed', () =>
- this.handleShowBackgroundContent()
- );
}
override connectedCallback() {
super.connectedCallback();
- this.jsAPI.addElement(TargetElement.CHANGE_ACTIONS, this);
+ this.getPluginLoader().jsApiService.addElement(
+ TargetElement.CHANGE_ACTIONS,
+ this
+ );
this.handleLoadingComplete();
}
static override get styles() {
return [
sharedStyles,
+ modalStyles,
css`
:host {
display: flex;
@@ -669,7 +669,7 @@
<span id="moreMessage">More</span>
</gr-dropdown>
</div>
- <gr-overlay id="overlay" with-backdrop="">
+ <dialog id="actionsModal" tabindex="-1">
<gr-confirm-rebase-dialog
id="confirmRebase"
class="confirmDialog"
@@ -768,7 +768,7 @@
Do you really want to delete the edit?
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -883,17 +883,14 @@
}
private handleLoadingComplete() {
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => (this.loading = false));
}
// private but used in test
- sendShowRevisionActions(detail: {
- change: ChangeInfo;
- revisionActions: ActionNameToActionInfoMap;
- }) {
- this.jsAPI.handleEvent(PluginEventType.SHOW_REVISION_ACTIONS, detail);
+ sendShowRevisionActions(detail: ShowRevisionActionsDetail) {
+ this.getPluginLoader().jsApiService.handleShowRevisionActions(detail);
}
addActionButton(type: ActionType, label: string) {
@@ -1335,7 +1332,7 @@
if (!this.change) {
return false;
}
- return this.jsAPI.canSubmitChange(
+ return this.getPluginLoader().jsApiService.canSubmitChange(
this.change,
this.getRevision(this.change, this.latestPatchNum)
);
@@ -1565,20 +1562,20 @@
for (const dialogEl of dialogEls) {
(dialogEl as HTMLElement).hidden = true;
}
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.close();
+ assertIsDefined(this.actionsModal, 'actionsModal');
+ this.actionsModal.close();
}
// private but used in test
handleRebaseConfirm(e: CustomEvent<ConfirmRebaseEventDetail>) {
assertIsDefined(this.confirmRebase, 'confirmRebase');
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.actionsModal, 'actionsModal');
const el = this.confirmRebase;
const payload = {
base: e.detail.base,
allow_conflicts: e.detail.allowConflicts,
};
- this.overlay.close();
+ this.actionsModal.close();
el.hidden = true;
this.fireAction(
'/rebase',
@@ -1601,7 +1598,7 @@
private handleCherryPickRestApi(conflicts: boolean) {
assertIsDefined(this.confirmCherrypick, 'confirmCherrypick');
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.actionsModal, 'actionsModal');
const el = this.confirmCherrypick;
if (!el.branch) {
fireAlert(this, ERR_BRANCH_EMPTY);
@@ -1611,7 +1608,7 @@
fireAlert(this, ERR_COMMIT_EMPTY);
return;
}
- this.overlay.close();
+ this.actionsModal.close();
el.hidden = true;
this.fireAction(
'/cherrypick',
@@ -1629,13 +1626,13 @@
// private but used in test
handleMoveConfirm() {
assertIsDefined(this.confirmMove, 'confirmMove');
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.actionsModal, 'actionsModal');
const el = this.confirmMove;
if (!el.branch) {
fireAlert(this, ERR_BRANCH_EMPTY);
return;
}
- this.overlay.close();
+ this.actionsModal.close();
el.hidden = true;
this.fireAction('/move', assertUIActionInfo(this.actions.move), false, {
destination_branch: el.branch,
@@ -1645,11 +1642,11 @@
private handleRevertDialogConfirm(e: CustomEvent<ConfirmRevertEventDetail>) {
assertIsDefined(this.confirmRevertDialog, 'confirmRevertDialog');
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.actionsModal, 'actionsModal');
const revertType = e.detail.revertType;
const message = e.detail.message;
const el = this.confirmRevertDialog;
- this.overlay.close();
+ this.actionsModal.close();
el.hidden = true;
switch (revertType) {
case RevertType.REVERT_SINGLE_CHANGE:
@@ -1681,9 +1678,9 @@
// private but used in test
handleAbandonDialogConfirm() {
assertIsDefined(this.confirmAbandonDialog, 'confirmAbandonDialog');
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.actionsModal, 'actionsModal');
const el = this.confirmAbandonDialog;
- this.overlay.close();
+ this.actionsModal.close();
el.hidden = true;
this.fireAction(
'/abandon',
@@ -1702,8 +1699,8 @@
}
private handleCloseCreateFollowUpChange() {
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.close();
+ assertIsDefined(this.actionsModal, 'actionsModal');
+ this.actionsModal.close();
}
private handleDeleteConfirm() {
@@ -1720,7 +1717,7 @@
// We need to make sure that all cached version of a change
// edit are deleted.
- this.storage.eraseEditableContentItemsForChangeEdit(this.changeNum);
+ this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
this.fireAction(
'/edit',
@@ -1817,8 +1814,9 @@
this.hideAllDialogs();
if (dialog.init) dialog.init();
dialog.hidden = false;
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.open().then(() => {
+ assertIsDefined(this.actionsModal, 'actionsModal');
+ this.actionsModal.showModal();
+ whenVisible(dialog, () => {
if (dialog.resetFocus) {
dialog.resetFocus();
}
@@ -1829,7 +1827,9 @@
// https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
// private but used in test
setReviewOnRevert(newChangeId: NumericChangeId) {
- const review = this.jsAPI.getReviewPostRevert(this.change);
+ const review = this.getPluginLoader().jsApiService.getReviewPostRevert(
+ this.change
+ );
if (!review) {
return Promise.resolve(undefined);
}
@@ -2077,7 +2077,7 @@
// We need to make sure that all cached version of a change
// edit are deleted.
- this.storage.eraseEditableContentItemsForChangeEdit(this.changeNum);
+ this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
this.fireAction(
'/edit:publish',
@@ -2098,18 +2098,6 @@
);
}
- // private but used in test
- handleHideBackgroundContent() {
- assertIsDefined(this.mainContent, 'mainContent');
- this.mainContent.classList.add('overlayOpen');
- }
-
- // private but used in test
- handleShowBackgroundContent() {
- assertIsDefined(this.mainContent, 'mainContent');
- this.mainContent.classList.remove('overlayOpen');
- }
-
/**
* Merge sources of change actions into a single ordered array of action
* values.
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 1b98c5d..3ba04f7 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
@@ -6,7 +6,6 @@
import '../../../test/common-test-setup';
import './gr-change-actions';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {
createAccountWithId,
createApproval,
@@ -22,7 +21,6 @@
query,
queryAll,
queryAndAssert,
- spyStorage,
stubReporting,
stubRestApi,
} from '../../../test/test-utils';
@@ -48,11 +46,9 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {UIActionInfo} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
-import {getAppContext} from '../../../services/app-context';
import {fixture, html, assert} from '@open-wc/testing';
import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
import {GrConfirmRebaseDialog} from '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog';
import {GrConfirmMoveDialog} from '../gr-confirm-move-dialog/gr-confirm-move-dialog';
@@ -60,6 +56,8 @@
import {GrConfirmRevertDialog} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
suite('gr-change-actions tests', () => {
@@ -120,7 +118,7 @@
});
sinon
- .stub(getPluginLoader(), 'awaitPluginsLoaded')
+ .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
.returns(Promise.resolve());
element = await fixture<GrChangeActions>(html`
@@ -208,13 +206,7 @@
<span id="moreMessage"> More </span>
</gr-dropdown>
</div>
- <gr-overlay
- aria-hidden="true"
- id="overlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="actionsModal" tabindex="-1">
<gr-confirm-rebase-dialog class="confirmDialog" id="confirmRebase">
</gr-confirm-rebase-dialog>
<gr-confirm-cherrypick-dialog
@@ -280,7 +272,7 @@
Do you really want to delete the edit?
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -480,9 +472,6 @@
stubRestApi('getFromProjectLookup').returns(
Promise.resolve('test' as RepoName)
);
- sinon
- .stub(queryAndAssert<GrOverlay>(element, '#overlay'), 'open')
- .returns(Promise.resolve());
element.change = {
...createChangeViewChange(),
revisions: {
@@ -519,9 +508,6 @@
stubRestApi('getFromProjectLookup').returns(
Promise.resolve('test' as RepoName)
);
- sinon
- .stub(queryAndAssert<GrOverlay>(element, '#overlay'), 'open')
- .returns(Promise.resolve());
element.change = {
...createChangeViewChange(),
revisions: {
@@ -714,42 +700,15 @@
);
});
- test('fullscreen-overlay-opened hides content', () => {
- const spy = sinon.spy(element, 'handleHideBackgroundContent');
- queryAndAssert<GrOverlay>(element, '#overlay').dispatchEvent(
- new CustomEvent('fullscreen-overlay-opened', {
- composed: true,
- bubbles: true,
- })
- );
- assert.isTrue(spy.called);
- assert.isTrue(
- queryAndAssert<Element>(element, '#mainContent').classList.contains(
- 'overlayOpen'
- )
- );
- });
-
- test('fullscreen-overlay-closed shows content', () => {
- const spy = sinon.spy(element, 'handleShowBackgroundContent');
- queryAndAssert<GrOverlay>(element, '#overlay').dispatchEvent(
- new CustomEvent('fullscreen-overlay-closed', {
- composed: true,
- bubbles: true,
- })
- );
- assert.isTrue(spy.called);
- assert.isFalse(
- queryAndAssert<Element>(element, '#mainContent').classList.contains(
- 'overlayOpen'
- )
- );
- });
-
test('setReviewOnRevert', () => {
const review = {labels: {Foo: 1, 'Bar-Baz': -2}};
const changeId = 1234 as NumericChangeId;
- sinon.stub(element.jsAPI, 'getReviewPostRevert').returns(review);
+ sinon
+ .stub(
+ testResolver(pluginLoaderToken).jsApiService,
+ 'getReviewPostRevert'
+ )
+ .returns(review);
const saveStub = stubRestApi('saveChangeReview').returns(
Promise.resolve(new Response())
);
@@ -813,7 +772,7 @@
element.editPatchsetLoaded = true;
await element.updateComplete;
- const storage = getAppContext().storageService;
+ const storage = testResolver(storageServiceToken);
storage.setEditableContentItem(
'c42_ps2_index.php',
'<?php\necho 42_ps_2'
@@ -836,7 +795,8 @@
assert.isOk(storage.getEditableContentItem('c42_ps2_index.php')!);
assert.isNotOk(storage.getEditableContentItem('c50_psedit_index.php')!);
- const eraseEditableContentItemsForChangeEditSpy = spyStorage(
+ const eraseEditableContentItemsForChangeEditSpy = sinon.spy(
+ storage,
'eraseEditableContentItemsForChangeEdit'
);
sinon.stub(element, 'fireAction');
@@ -2685,7 +2645,7 @@
stubRestApi('send').returns(Promise.reject(new Error('error')));
sinon
- .stub(getPluginLoader(), 'awaitPluginsLoaded')
+ .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
.returns(Promise.resolve());
element = await fixture<GrChangeActions>(html`
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 228e7ce..18c1455 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -61,7 +61,7 @@
import {fireAlert, fireEvent, fireReload} from '../../../utils/event-util';
import {
EditRevisionInfo,
- notUndefined,
+ isDefined,
ParsedChangeInfo,
} from '../../../types/types';
import {
@@ -891,14 +891,14 @@
private computeProjectUrl(project?: RepoName) {
if (!project) return '';
- return createSearchUrl({project});
+ return createSearchUrl({repo: project});
}
private computeBranchUrl(project?: RepoName, branch?: BranchName) {
if (!project || !branch || !this.change || !this.change.status) return '';
return createSearchUrl({
branch,
- project,
+ repo: project,
statuses:
this.change.status === ChangeStatus.NEW
? ['open']
@@ -916,7 +916,7 @@
}
return createChangeUrl({
changeNum: change,
- project,
+ repo: project,
usp: 'metadata',
patchNum: patchset,
});
@@ -1124,7 +1124,7 @@
.then(response =>
(response ?? [])
.map(change => change.topic)
- .filter(notUndefined)
+ .filter(isDefined)
.filter(unique)
.map(topic => {
return {name: topic, value: topic};
@@ -1140,7 +1140,7 @@
.then(response =>
(response ?? [])
.flatMap(change => change.hashtags ?? [])
- .filter(notUndefined)
+ .filter(isDefined)
.filter(unique)
.map(hashtag => {
return {name: hashtag, value: hashtag};
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 038c34a..50bb9d9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -5,7 +5,7 @@
*/
import '../../../test/common-test-setup';
import './gr-change-metadata';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+
import {ChangeRole, GrChangeMetadata} from './gr-change-metadata';
import {
createServerInfo,
@@ -46,7 +46,6 @@
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {
queryAndAssert,
- resetPlugins,
stubRestApi,
waitUntilCalled,
} from '../../../test/test-utils';
@@ -56,6 +55,8 @@
import {nothing} from 'lit';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-change-metadata tests', () => {
let element: GrChangeMetadata;
@@ -949,17 +950,12 @@
suite('plugin endpoints', () => {
setup(async () => {
- resetPlugins();
element = await fixture(html`<gr-change-metadata></gr-change-metadata>`);
element.change = createParsedChange();
element.revision = createRevision();
await element.updateComplete;
});
- teardown(() => {
- resetPlugins();
- });
-
test('endpoint params', async () => {
interface MetadataGrEndpointDecorator extends GrEndpointDecorator {
plugin: PluginApi;
@@ -978,7 +974,7 @@
const hookEl = (await plugin!
.hook('change-metadata-item')
.getLastAttached()) as MetadataGrEndpointDecorator;
- getPluginLoader().loadPlugins([]);
+ testResolver(pluginLoaderToken).loadPlugins([]);
await element.updateComplete;
assert.strictEqual(hookEl.plugin, plugin!);
assert.strictEqual(hookEl.change, element.change);
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 73653bb..84bdffb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -40,7 +40,7 @@
} from '../../../utils/comment-util';
import {pluralize} from '../../../utils/string-util';
import {AccountInfo} from '../../../types/common';
-import {notUndefined} from '../../../types/types';
+import {isDefined} from '../../../types/types';
import {Tab} from '../../../constants/constants';
import {ChecksTabState, CommentTabState} from '../../../types/events';
import {spinnerStyles} from '../../../styles/gr-spinner-styles';
@@ -57,6 +57,7 @@
import {when} from 'lit/directives/when.js';
import {KnownExperimentId} from '../../../services/flags/flags';
import {combineLatest} from 'rxjs';
+import {userModelToken} from '../../../models/user/user-model';
function handleSpaceOrEnter(e: KeyboardEvent, handler: () => void) {
if (modifierPressed(e)) return;
@@ -109,11 +110,9 @@
private readonly showAllChips = new Map<RunStatus | Category, boolean>();
- // private but used in tests
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
- // private but used in tests
- readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getChecksModel = resolve(this, checksModelToken);
@@ -172,7 +171,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.selfAccount = x)
);
if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
@@ -180,7 +179,7 @@
this,
() =>
combineLatest([
- this.userModel.account$,
+ this.getUserModel().account$,
this.getCommentsModel().threads$,
]),
([selfAccount, threads]) => {
@@ -670,7 +669,7 @@
return commentThreads
.map(getFirstComment)
.map(comment => comment?.author ?? this.selfAccount)
- .filter(notUndefined);
+ .filter(isDefined);
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 9584637..05036ab 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -16,11 +16,22 @@
} from '../../../test/test-data-generators';
import {stubFlags} from '../../../test/test-utils';
import {Timestamp} from '../../../api/rest-api';
+import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
suite('gr-change-summary test', () => {
let element: GrChangeSummary;
+ let commentsModel: CommentsModel;
+ let userModel: UserModel;
+
setup(async () => {
element = await fixture(html`<gr-change-summary></gr-change-summary>`);
+ commentsModel = testResolver(commentsModelToken);
+ userModel = testResolver(userModelToken);
});
test('is defined', () => {
@@ -29,7 +40,7 @@
});
test('renders', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
drafts: {
a: [createDraft(), createDraft(), createDraft()],
},
@@ -112,7 +123,7 @@
element = await fixture(html`<gr-change-summary></gr-change-summary>`);
await element.updateComplete;
- element.getCommentsModel().setState({
+ commentsModel.setState({
drafts: {
a: [
{
@@ -139,7 +150,7 @@
},
discardedDrafts: [],
});
- element.userModel.setAccount({
+ userModel.setAccount({
...createAccountWithEmail('abc@def.com'),
registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
});
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 c62e4af..6d76664 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
@@ -42,8 +42,6 @@
windowLocationReload,
} from '../../../utils/dom-util';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
import {
ChangeStatus,
@@ -69,7 +67,6 @@
isInvolved,
roleDetails,
} from '../../../utils/change-util';
-import {EventType as PluginEventType} from '../../../api/plugin';
import {customElement, property, query, state} from 'lit/decorators.js';
import {GrApplyFixDialog} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
import {GrFileListHeader} from '../gr-file-list-header/gr-file-list-header';
@@ -108,7 +105,12 @@
import {GrIncludedInDialog} from '../gr-included-in-dialog/gr-included-in-dialog';
import {GrDownloadDialog} from '../gr-download-dialog/gr-download-dialog';
import {GrChangeMetadata} from '../gr-change-metadata/gr-change-metadata';
-import {assertIsDefined, assert, queryAll} from '../../../utils/common-util';
+import {
+ assertIsDefined,
+ assert,
+ queryAll,
+ queryAndAssert,
+} from '../../../utils/common-util';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {
CommentThread,
@@ -139,7 +141,10 @@
fireReload,
fireTitleChange,
} from '../../../utils/event-util';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {
debounce,
DelayedTask,
@@ -185,21 +190,15 @@
} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
import {createEditUrl} from '../../../models/views/edit';
-
-const CHANGE_ID_ERROR = {
- MISMATCH: 'mismatch',
- MISSING: 'missing',
-};
-const CHANGE_ID_REGEX_PATTERN =
- /^(Change-Id:\s|Link:.*\/id\/)(I[0-9a-f]{8,40})/gm;
+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';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
const REVIEWERS_REGEX = /^(R|CC)=/gm;
const MIN_CHECK_INTERVAL_SECS = 0;
-const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
-
const ACCIDENTAL_STARRING_LIMIT_MS = 10 * 1000;
const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
@@ -251,11 +250,11 @@
@query('#includedInDialog') includedInDialog?: GrIncludedInDialog;
- @query('#downloadOverlay') downloadOverlay?: GrOverlay;
+ @query('#downloadModal') downloadModal?: HTMLDialogElement;
@query('#downloadDialog') downloadDialog?: GrDownloadDialog;
- @query('#replyOverlay') replyOverlay?: GrOverlay;
+ @query('#replyModal') replyModal?: HTMLDialogElement;
@query('#replyDialog') replyDialog?: GrReplyDialog;
@@ -396,14 +395,6 @@
@state()
selectedRevision?: RevisionInfo | EditRevisionInfo;
- @state()
- get changeIdCommitMessageError() {
- return this.computeChangeIdCommitMessageError(
- this.latestCommitMessage,
- this.change
- );
- }
-
/**
* <gr-change-actions> populates this via two-way data binding.
* Private but used in tests.
@@ -539,31 +530,28 @@
@state()
scrollCommentId?: UrlEncodedCommentId;
- /** Just reflects the `opened` prop of the overlay. */
+ /** Reflects the `opened` state of the reply dialog. */
@state()
- private replyOverlayOpened = false;
+ replyModalOpened = false;
// Accessed in tests.
readonly reporting = getAppContext().reportingService;
- readonly jsAPI = getAppContext().jsApiService;
-
private readonly getChecksModel = resolve(this, checksModelToken);
readonly restApiService = getAppContext().restApiService;
private readonly flagsService = getAppContext().flagsService;
- // Private but used in tests.
- readonly userModel = getAppContext().userModel;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
- // Private but used in tests.
- readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getUserModel = resolve(this, userModelToken);
- private readonly routerModel = getAppContext().routerModel;
+ private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getRouterModel = resolve(this, routerModelToken);
+
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getConfigModel = resolve(this, configModelToken);
@@ -573,8 +561,6 @@
private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
- private replyRefitTask?: DelayedTask;
-
private scrollTask?: DelayedTask;
private lastStarredTimestamp?: number;
@@ -611,18 +597,6 @@
}
private setupListeners() {
- this.addEventListener(
- // When an overlay is opened in a mobile viewport, the overlay has a full
- // screen view. When it has a full screen view, we do not want the
- // background to be scrollable. This will eliminate background scroll by
- // hiding most of the contents on the screen upon opening, and showing
- // again upon closing.
- 'fullscreen-overlay-opened',
- () => this.handleHideBackgroundContent()
- );
- this.addEventListener('fullscreen-overlay-closed', () =>
- this.handleShowBackgroundContent()
- );
this.addEventListener('open-reply-dialog', () => this.openReplyDialog());
this.addEventListener('change-message-deleted', () => fireReload(this));
this.addEventListener('editable-content-save', e =>
@@ -736,14 +710,14 @@
);
subscribe(
this,
- () => this.routerModel.routerView$,
+ () => this.getRouterModel().routerView$,
view => {
this.isViewCurrent = view === GerritView.CHANGE;
}
);
subscribe(
this,
- () => this.routerModel.routerPatchNum$,
+ () => this.getRouterModel().routerPatchNum$,
patchNum => {
this.routerPatchNum = patchNum;
}
@@ -757,7 +731,7 @@
);
subscribe(
this,
- () => this.userModel.preferenceDiffViewMode$,
+ () => this.getUserModel().preferenceDiffViewMode$,
diffViewMode => {
this.diffViewMode = diffViewMode;
}
@@ -787,14 +761,14 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
account => {
this.account = account;
}
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => {
this.loggedIn = loggedIn;
}
@@ -846,13 +820,17 @@
if (!this.isFirstConnection) return;
this.isFirstConnection = false;
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
this.pluginTabsHeaderEndpoints =
- getPluginEndpoints().getDynamicEndpoints('change-view-tab-header');
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-view-tab-header'
+ );
this.pluginTabsContentEndpoints =
- getPluginEndpoints().getDynamicEndpoints('change-view-tab-content');
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-view-tab-content'
+ );
if (
this.pluginTabsContentEndpoints.length !==
this.pluginTabsHeaderEndpoints.length
@@ -876,7 +854,6 @@
this.handleVisibilityChange
);
document.removeEventListener('scroll', this.handleScroll);
- this.replyRefitTask?.cancel();
this.scrollTask?.cancel();
if (this.updateCheckTimerHandle) {
@@ -901,6 +878,7 @@
a11yStyles,
paperStyles,
sharedStyles,
+ modalStyles,
css`
.container:not(.loading) {
background-color: var(--background-color-tertiary);
@@ -976,11 +954,6 @@
background-color: var(--background-color-secondary);
padding-right: var(--spacing-m);
}
- .changeId {
- color: var(--deemphasized-text-color);
- font-family: var(--font-family);
- margin-top: var(--spacing-l);
- }
section {
background-color: var(--view-background-color);
box-shadow: var(--elevation-level-1);
@@ -1183,19 +1156,11 @@
flex: initial;
margin: 0;
}
- /* Change actions are the only thing thant need to remain visible due
- to the fact that they may have the currently visible overlay open. */
- #mainContent.overlayOpen .hideOnMobileOverlay {
- display: none;
- }
gr-reply-dialog {
height: 100vh;
min-width: initial;
width: 100vw;
}
- #replyOverlay {
- z-index: var(--reply-overlay-z-index);
- }
}
.patch-set-dropdown {
margin: var(--spacing-m) 0 0 var(--spacing-m);
@@ -1239,7 +1204,7 @@
.change=${this.change}
.changeNum=${this.changeNum}
></gr-apply-fix-dialog>
- <gr-overlay id="downloadOverlay" with-backdrop="">
+ <dialog id="downloadModal" tabindex="-1">
<gr-download-dialog
id="downloadDialog"
.change=${this.change}
@@ -1247,7 +1212,7 @@
.config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
- </gr-overlay>
+ </dialog>
<gr-overlay id="includedInOverlay" with-backdrop="">
<gr-included-in-dialog
id="includedInDialog"
@@ -1255,18 +1220,9 @@
@close=${this.handleIncludedInDialogClose}
></gr-included-in-dialog>
</gr-overlay>
- <gr-overlay
- id="replyOverlay"
- class="scrollable"
- no-cancel-on-outside-click=""
- no-cancel-on-esc-key=""
- scroll-action="lock"
- with-backdrop=""
- @iron-overlay-canceled=${this.onReplyOverlayCanceled}
- @opened-changed=${this.onReplyOverlayOpenedChanged}
- >
+ <dialog id="replyModal" @close=${this.onReplyModalCanceled}>
${when(
- this.replyOverlayOpened && this.loggedIn,
+ this.replyModalOpened && this.loggedIn,
() => html`
<gr-reply-dialog
id="replyDialog"
@@ -1276,13 +1232,11 @@
.canBeStarted=${this.canStartReview()}
@send=${this.handleReplySent}
@cancel=${this.handleReplyCancel}
- @autogrow=${this.handleReplyAutogrow}
- @send-disabled-changed=${this.resetReplyOverlayFocusStops}
>
</gr-reply-dialog>
`
)}
- </gr-overlay>
+ </dialog>
`;
}
@@ -1453,7 +1407,7 @@
this.getEditMode()
);
return html` <div class="changeInfo">
- <div class="changeInfo-column changeMetadata hideOnMobileOverlay">
+ <div class="changeInfo-column changeMetadata">
<gr-change-metadata
id="metadata"
.change=${this.change}
@@ -1469,7 +1423,7 @@
</gr-change-metadata>
</div>
<div id="mainChangeInfo" class="changeInfo-column mainChangeInfo">
- <div id="commitAndRelated" class="hideOnMobileOverlay">
+ <div id="commitAndRelated">
<div class="commitContainer">
<h3 class="assistive-tech-only">Commit Message</h3>
<div>
@@ -1504,19 +1458,6 @@
.markdown=${false}
></gr-formatted-text>
</gr-editable-content>
- <div class="changeId" ?hidden=${!this.changeIdCommitMessageError}>
- <hr />
- Change-Id:
- <span
- class=${this.computeChangeIdClass(
- this.changeIdCommitMessageError
- )}
- title=${this.computeTitleAttributeWarning(
- this.changeIdCommitMessageError
- )}
- >${this.change?.change_id}</span
- >
- </div>
</div>
<h3 class="assistive-tech-only">Comments and Checks Summary</h3>
<gr-change-summary></gr-change-summary>
@@ -1636,7 +1577,6 @@
</gr-file-list-header>
<gr-file-list
id="fileList"
- class="hideOnMobileOverlay"
.change=${this.change}
.changeNum=${this.changeNum}
.patchRange=${this.patchRange}
@@ -1742,7 +1682,6 @@
<section class="changeLog">
<h2 class="assistive-tech-only">Change Log</h2>
<gr-messages-list
- class="hideOnMobileOverlay"
.labels=${this.change?.labels}
.messages=${this.change?.messages}
.reviewerUpdates=${this.change?.reviewer_updates}
@@ -1774,9 +1713,9 @@
// Private but used in tests.
handleToggleDiffMode() {
if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
- this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+ this.getUserModel().updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
}
@@ -1865,7 +1804,10 @@
// Trim trailing whitespace from each line.
const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
- this.jsAPI.handleCommitMessage(this.change, message);
+ this.getPluginLoader().jsApiService.handleCommitMessage(
+ this.change,
+ message
+ );
this.commitMessageEditor.disabled = true;
this.restApiService
@@ -2022,13 +1964,10 @@
this.openReplyDialog(FocusTarget.ANY);
}
- private onReplyOverlayCanceled() {
+ private onReplyModalCanceled() {
fireDialogChange(this, {canceled: true});
this.changeViewAriaHidden = false;
- }
-
- private onReplyOverlayOpenedChanged(e: ValueChangedEvent<boolean>) {
- this.replyOverlayOpened = e.detail.value;
+ this.replyModalOpened = false;
}
private handleOpenDiffPrefs() {
@@ -2054,18 +1993,31 @@
// Private but used in tests
handleOpenDownloadDialog() {
- assertIsDefined(this.downloadOverlay);
- this.downloadOverlay.open().then(() => {
- assertIsDefined(this.downloadOverlay);
+ assertIsDefined(this.downloadModal);
+ this.downloadModal.showModal();
+ whenVisible(this.downloadModal, () => {
+ assertIsDefined(this.downloadModal);
assertIsDefined(this.downloadDialog);
- this.downloadOverlay.setFocusStops(this.downloadDialog.getFocusStops());
this.downloadDialog.focus();
+ const downloadCommands = queryAndAssert(
+ this.downloadDialog,
+ 'gr-download-commands'
+ );
+ const paperTabs = queryAndAssert<PaperTabsElement>(
+ downloadCommands,
+ 'paper-tabs'
+ );
+ // Paper Tabs normally listen to 'iron-resize' event to call this method.
+ // After migrating to Dialog element, this event is no longer fired
+ // which means this method is not called which ends up styling the
+ // selected paper tab with an underline.
+ paperTabs._onTabSizingChanged();
});
}
private handleDownloadDialogClose() {
- assertIsDefined(this.downloadOverlay);
- this.downloadOverlay.close();
+ assertIsDefined(this.downloadModal);
+ this.downloadModal.close();
}
// Private but used in tests.
@@ -2080,18 +2032,6 @@
}
// Private but used in tests.
- handleHideBackgroundContent() {
- assertIsDefined(this.mainContent);
- this.mainContent.classList.add('overlayOpen');
- }
-
- // Private but used in tests.
- handleShowBackgroundContent() {
- assertIsDefined(this.mainContent);
- this.mainContent.classList.remove('overlayOpen');
- }
-
- // Private but used in tests.
handleReplySent() {
this.addEventListener(
'change-details-loaded',
@@ -2100,26 +2040,15 @@
},
{once: true}
);
- assertIsDefined(this.replyOverlay);
- this.replyOverlay.cancel();
+ assertIsDefined(this.replyModal);
+ this.replyModal.close();
fireReload(this);
}
private handleReplyCancel() {
- assertIsDefined(this.replyOverlay);
- this.replyOverlay.cancel();
- }
-
- private handleReplyAutogrow() {
- // If the textarea resizes, we need to re-fit the overlay.
- this.replyRefitTask = debounce(
- this.replyRefitTask,
- () => {
- assertIsDefined(this.replyOverlay);
- this.replyOverlay.refit();
- },
- REPLY_REFIT_DEBOUNCE_INTERVAL_MS
- );
+ assertIsDefined(this.replyModal);
+ this.replyModal.close();
+ this.onReplyModalCanceled();
}
// Private but used in tests.
@@ -2200,10 +2129,10 @@
return;
}
- if (this.viewState.changeNum && this.viewState.project) {
+ if (this.viewState.changeNum && this.viewState.repo) {
this.restApiService.setInProjectLookup(
this.viewState.changeNum,
- this.viewState.project
+ this.viewState.repo
);
}
@@ -2285,7 +2214,7 @@
this.performPostLoadTasks();
});
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
this.initActiveTab();
@@ -2305,7 +2234,7 @@
// Private but used in tests.
sendShowChangeEvent() {
assertIsDefined(this.patchRange, 'patchRange');
- this.jsAPI.handleEvent(PluginEventType.SHOW_CHANGE, {
+ this.getPluginLoader().jsApiService.handleShowChange({
change: this.change,
patchNum: this.patchRange.patchNum,
info: {mergeable: this.mergeable},
@@ -2366,7 +2295,7 @@
// Private but used in tests.
maybeShowRevertDialog() {
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
if (
@@ -2473,60 +2402,6 @@
});
}
- // private but used in test
- computeChangeIdClass(displayChangeId?: string | null) {
- if (displayChangeId) {
- return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
- }
- return '';
- }
-
- computeTitleAttributeWarning(displayChangeId?: string | null) {
- if (!displayChangeId) {
- return undefined;
- }
- if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) {
- return 'Change-Id mismatch';
- } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) {
- return 'No Change-Id in commit message';
- }
- return undefined;
- }
-
- computeChangeIdCommitMessageError(
- commitMessage: string | null,
- change?: ParsedChangeInfo
- ) {
- if (change === undefined) {
- return undefined;
- }
-
- if (!commitMessage) {
- return CHANGE_ID_ERROR.MISSING;
- }
-
- // Find the last match in the commit message:
- let changeId;
- let changeIdArr;
-
- while ((changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage))) {
- changeId = changeIdArr[2];
- }
-
- if (changeId) {
- // A change-id is detected in the commit message.
-
- if (changeId === change.change_id) {
- // The change-id found matches the real change-id.
- return null;
- }
- // The change-id found does not match the change-id.
- return CHANGE_ID_ERROR.MISMATCH;
- }
- // There is no change-id in the commit message.
- return CHANGE_ID_ERROR.MISSING;
- }
-
// Private but used in tests.
computeReplyButtonLabel() {
if (this.diffDrafts === undefined) {
@@ -2745,23 +2620,19 @@
return;
}
this.handleLabelRemoved(oldLabels, newLabels);
- this.jsAPI.handleEvent(PluginEventType.LABEL_CHANGE, {
+ this.getPluginLoader().jsApiService.handleLabelChange({
change: this.change,
});
}
openReplyDialog(focusTarget?: FocusTarget, quote?: string) {
if (!this.change) return;
- assertIsDefined(this.replyOverlay);
- const overlay = this.replyOverlay;
- overlay.open().finally(() => {
- // the following code should be executed no matter open succeed or not
- const dialog = this.replyDialog;
- assertIsDefined(dialog, 'reply dialog');
- this.resetReplyOverlayFocusStops();
- dialog.open(focusTarget, quote);
- const observer = new ResizeObserver(() => overlay.center());
- observer.observe(dialog);
+ this.replyModalOpened = true;
+ assertIsDefined(this.replyModal);
+ this.replyModal.showModal();
+ whenVisible(this.replyModal, () => {
+ assertIsDefined(this.replyDialog, 'replyDialog');
+ this.replyDialog.open(focusTarget, quote);
});
fireDialogChange(this, {opened: true});
this.changeViewAriaHidden = true;
@@ -2845,6 +2716,11 @@
change => change?.status === ChangeStatus.MERGED
);
if (!this.changeStatuses) return;
+ // Protect against `computeRevertSubmitted()` being called twice.
+ // TODO: Convert this to be rxjs based, so computeRevertSubmitted() is not
+ // actively called, but instead we can subscribe to something.
+ if (this.changeStatuses.includes(ChangeStates.REVERT_SUBMITTED)) return;
+ if (this.changeStatuses.includes(ChangeStates.REVERT_CREATED)) return;
if (submittedRevert) {
this.revertedChange = submittedRevert;
this.changeStatuses = this.changeStatuses.concat([
@@ -3325,7 +3201,7 @@
this.getNavigation().setUrl(
createEditUrl({
changeNum: this.change._number,
- project: this.change.project,
+ repo: this.change.project,
path,
patchNum: this.patchRange.patchNum,
})
@@ -3403,14 +3279,6 @@
);
}
- private resetReplyOverlayFocusStops() {
- const dialog = this.replyDialog;
- const focusStops = dialog?.getFocusStops();
- if (!focusStops) return;
- assertIsDefined(this.replyOverlay);
- this.replyOverlay.setFocusStops(focusStops);
- }
-
// Private but used in tests.
async handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
if (e.detail.starred) {
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 d86007a..748bb36 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
@@ -12,26 +12,23 @@
CommentSide,
DefaultBase,
DiffViewMode,
- HttpMethod,
MessageTag,
createDefaultPreferences,
Tab,
} from '../../../constants/constants';
import {GrEditConstants} from '../../edit/gr-edit-constants';
-import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {EventType, PluginApi} from '../../../api/plugin';
+import {PluginApi} from '../../../api/plugin';
import {
mockPromise,
pressKey,
queryAndAssert,
stubFlags,
stubRestApi,
- stubUsers,
waitEventLoop,
waitQueryAndAssert,
waitUntil,
+ waitUntilVisible,
} from '../../../test/test-utils';
import {
createChangeViewState,
@@ -77,6 +74,7 @@
DetailedLabelInfo,
RepoName,
QuickLabelInfo,
+ PatchSetNumber,
} from '../../../types/common';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
@@ -86,9 +84,12 @@
import {ParsedChangeInfo} from '../../../types/types';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
-import {LoadingStatus} from '../../../models/change/change-model';
+import {
+ ChangeModel,
+ changeModelToken,
+ LoadingStatus,
+} from '../../../models/change/change-model';
import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
import {GrThreadList} from '../gr-thread-list/gr-thread-list';
import {assertIsDefined} from '../../../utils/common-util';
@@ -101,10 +102,19 @@
import {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';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-change-view tests', () => {
let element: GrChangeView;
let setUrlStub: sinon.SinonStub;
+ let userModel: UserModel;
+ let changeModel: ChangeModel;
+ let commentsModel: CommentsModel;
const ROBOT_COMMENTS_LIMIT = 10;
@@ -327,8 +337,6 @@
];
setup(async () => {
- // Since pluginEndpoints are global, must reset state.
- _testOnly_resetEndpoints();
setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
stubRestApi('getConfig').returns(
@@ -347,7 +355,6 @@
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
- getPluginLoader().loadPlugins([]);
window.Gerrit.install(
plugin => {
plugin.registerDynamicCustomComponent(
@@ -368,12 +375,15 @@
element.viewState = {
view: GerritView.CHANGE,
changeNum: TEST_NUMERIC_CHANGE_ID,
- project: 'gerrit' as RepoName,
+ repo: 'gerrit' as RepoName,
};
await element.updateComplete.then(() => {
assertIsDefined(element.actions);
sinon.stub(element.actions, 'reload').returns(Promise.resolve());
});
+ userModel = testResolver(userModelToken);
+ commentsModel = testResolver(commentsModelToken);
+ changeModel = testResolver(changeModelToken);
});
teardown(async () => {
@@ -408,11 +418,11 @@
</div>
<h2 class="assistive-tech-only">Change metadata</h2>
<div class="changeInfo">
- <div class="changeInfo-column changeMetadata hideOnMobileOverlay">
+ <div class="changeInfo-column changeMetadata">
<gr-change-metadata id="metadata"> </gr-change-metadata>
</div>
<div class="changeInfo-column mainChangeInfo" id="mainChangeInfo">
- <div class="hideOnMobileOverlay" id="commitAndRelated">
+ <div id="commitAndRelated">
<div class="commitContainer">
<h3 class="assistive-tech-only">Commit Message</h3>
<div>
@@ -435,11 +445,6 @@
>
<gr-formatted-text></gr-formatted-text>
</gr-editable-content>
- <div class="changeId" hidden="">
- <hr />
- Change-Id:
- <span class="" title=""></span>
- </div>
</div>
<h3 class="assistive-tech-only">
Comments and Checks Summary
@@ -499,8 +504,7 @@
<section class="tabContent">
<div>
<gr-file-list-header id="fileListHeader"> </gr-file-list-header>
- <gr-file-list class="hideOnMobileOverlay" id="fileList">
- </gr-file-list>
+ <gr-file-list id="fileList"> </gr-file-list>
</div>
</section>
<gr-endpoint-decorator name="change-view-integration">
@@ -521,20 +525,14 @@
</paper-tabs>
<section class="changeLog">
<h2 class="assistive-tech-only">Change Log</h2>
- <gr-messages-list class="hideOnMobileOverlay"> </gr-messages-list>
+ <gr-messages-list> </gr-messages-list>
</section>
</div>
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
- <gr-overlay
- aria-hidden="true"
- id="downloadOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="downloadModal" tabindex="-1">
<gr-download-dialog id="downloadDialog" role="dialog">
</gr-download-dialog>
- </gr-overlay>
+ </dialog>
<gr-overlay
aria-hidden="true"
id="includedInOverlay"
@@ -544,18 +542,7 @@
>
<gr-included-in-dialog id="includedInDialog"> </gr-included-in-dialog>
</gr-overlay>
- <gr-overlay
- aria-hidden="true"
- class="scrollable"
- id="replyOverlay"
- no-cancel-on-esc-key=""
- no-cancel-on-outside-click=""
- scroll-action="lock"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
- </gr-overlay>
+ <dialog id="replyModal"></dialog>
`
);
});
@@ -810,107 +797,61 @@
});
test('A fires an error event when not logged in', async () => {
- element.userModel.setAccount(undefined);
+ userModel.setAccount(undefined);
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
- assertIsDefined(element.replyOverlay);
- assert.isFalse(element.replyOverlay.opened);
+ assertIsDefined(element.replyModal);
+ assert.isFalse(element.replyModalOpened);
assert.isTrue(loggedInErrorSpy.called);
});
test('shift A does not open reply overlay', async () => {
pressKey(element, 'a', Modifier.SHIFT_KEY);
await element.updateComplete;
- assertIsDefined(element.replyOverlay);
- assert.isFalse(element.replyOverlay.opened);
+ assertIsDefined(element.replyModal);
+ assert.isFalse(element.replyModalOpened);
});
test('A toggles overlay when logged in', async () => {
- element.change = {
+ // restore clock so that setTimeout in waitUntil() works as expected
+ clock.restore();
+ stubRestApi('getChangeDetail').returns(
+ Promise.resolve(createParsedChange())
+ );
+ sinon.stub(element, 'performPostChangeLoadTasks');
+ sinon.stub(element, 'getMergeability');
+ const change = {
...createChangeViewChange(),
revisions: createRevisions(1),
messages: createChangeMessages(1),
};
- element.change.labels = {};
+ change.labels = {};
+ element.change = change;
+
+ changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change,
+ });
+
await element.updateComplete;
const openSpy = sinon.spy(element, 'openReplyDialog');
pressKey(element, 'a');
await element.updateComplete;
- assertIsDefined(element.replyOverlay);
- assert.isTrue(element.replyOverlay.opened);
- element.replyOverlay.close();
- assert.isFalse(element.replyOverlay.opened);
+ assertIsDefined(element.replyModal);
+ assert.isTrue(element.replyModalOpened);
+ sinon.spy(element.replyDialog!, 'open');
+ await waitUntilVisible(element.replyDialog!);
+ element.replyModal.close();
assert(
openSpy.lastCall.calledWithExactly(FocusTarget.ANY),
'openReplyDialog should have been passed ANY'
);
assert.equal(openSpy.callCount, 1);
- });
-
- test('fullscreen-overlay-opened hides content', async () => {
- element.loggedIn = true;
- element.loading = false;
- element.change = {
- ...createChangeViewChange(),
- labels: {},
- actions: {
- abandon: {
- enabled: true,
- label: 'Abandon',
- method: HttpMethod.POST,
- title: 'Abandon',
- },
- },
- };
- await element.updateComplete;
- const handlerSpy = sinon.spy(element, 'handleHideBackgroundContent');
- const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
- overlay.dispatchEvent(
- new CustomEvent('fullscreen-overlay-opened', {
- composed: true,
- bubbles: true,
- })
- );
- await element.updateComplete;
- assert.isTrue(handlerSpy.called);
- assertIsDefined(element.mainContent);
- assertIsDefined(element.actions);
- assert.isTrue(element.mainContent.classList.contains('overlayOpen'));
- assert.equal(getComputedStyle(element.actions).display, 'flex');
- });
-
- test('fullscreen-overlay-closed shows content', async () => {
- element.loggedIn = true;
- element.loading = false;
- element.change = {
- ...createChangeViewChange(),
- labels: {},
- actions: {
- abandon: {
- enabled: true,
- label: 'Abandon',
- method: HttpMethod.POST,
- title: 'Abandon',
- },
- },
- };
- await element.updateComplete;
- const handlerSpy = sinon.spy(element, 'handleShowBackgroundContent');
- const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
- overlay.dispatchEvent(
- new CustomEvent('fullscreen-overlay-closed', {
- composed: true,
- bubbles: true,
- })
- );
- await element.updateComplete;
- assert.isTrue(handlerSpy.called);
- assertIsDefined(element.mainContent);
- assert.isFalse(element.mainContent.classList.contains('overlayOpen'));
+ await waitUntil(() => !element.replyModalOpened);
});
test('expand all messages when expand-diffs fired', () => {
@@ -960,10 +901,8 @@
});
test('d should open download overlay', () => {
- assertIsDefined(element.downloadOverlay);
- const stub = sinon
- .stub(element.downloadOverlay, 'open')
- .returns(Promise.resolve());
+ assertIsDefined(element.downloadModal);
+ const stub = sinon.stub(element.downloadModal, 'showModal');
pressKey(element, 'd');
assert.isTrue(stub.called);
});
@@ -983,14 +922,14 @@
});
test('m should toggle diff mode', async () => {
- const updatePreferencesStub = stubUsers('updatePreferences');
+ const updatePreferencesStub = sinon.stub(userModel, 'updatePreferences');
await element.updateComplete;
const prefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
- element.userModel.setPreferences(prefs);
+ userModel.setPreferences(prefs);
element.handleToggleDiffMode();
assert.isTrue(
updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
@@ -1000,7 +939,7 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.UNIFIED,
};
- element.userModel.setPreferences(newPrefs);
+ userModel.setPreferences(newPrefs);
await element.updateComplete;
element.handleToggleDiffMode();
assert.isTrue(
@@ -1591,11 +1530,11 @@
sinon.stub(element, 'loadAndSetCommitInfo');
await element.updateComplete;
const reloadPortedCommentsStub = sinon.stub(
- element.getCommentsModel(),
+ commentsModel,
'reloadPortedComments'
);
const reloadPortedDraftsStub = sinon.stub(
- element.getCommentsModel(),
+ commentsModel,
'reloadPortedDrafts'
);
sinon.stub(element.fileList, 'collapseAllDiffs');
@@ -1688,7 +1627,7 @@
);
element.viewState = createChangeViewState();
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -1778,107 +1717,10 @@
element.handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
});
- test('computeChangeIdCommitMessageError', () => {
- let commitMessage = 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
- let change: ParsedChangeInfo = {
- ...createChangeViewChange(),
- change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId,
- };
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- null
- );
-
- change = {
- ...createChangeViewChange(),
- change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId,
- };
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch'
- );
-
- commitMessage = 'This is the greatest change.';
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- 'missing'
- );
- });
-
- test('multiple change Ids in commit message picks last', () => {
- const commitMessage = [
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
- ].join('\n');
- let change: ParsedChangeInfo = {
- ...createChangeViewChange(),
- change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId,
- };
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- null
- );
- change = {
- ...createChangeViewChange(),
- change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId,
- };
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch'
- );
- });
-
- test('does not count change Id that starts mid line', () => {
- const commitMessage = [
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
- ].join(' and ');
- let change: ParsedChangeInfo = {
- ...createChangeViewChange(),
- change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484' as ChangeId,
- };
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- null
- );
- change = {
- ...createChangeViewChange(),
- change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483' as ChangeId,
- };
- assert.equal(
- element.computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch'
- );
- });
-
- test('computeTitleAttributeWarning', () => {
- let changeIdCommitMessageError = 'missing';
- assert.equal(
- element.computeTitleAttributeWarning(changeIdCommitMessageError),
- 'No Change-Id in commit message'
- );
-
- changeIdCommitMessageError = 'mismatch';
- assert.equal(
- element.computeTitleAttributeWarning(changeIdCommitMessageError),
- 'Change-Id mismatch'
- );
- });
-
- test('computeChangeIdClass', () => {
- let changeIdCommitMessageError = 'missing';
- assert.equal(element.computeChangeIdClass(changeIdCommitMessageError), '');
-
- changeIdCommitMessageError = 'mismatch';
- assert.equal(
- element.computeChangeIdClass(changeIdCommitMessageError),
- 'warning'
- );
- });
test('topic is coalesced to null', async () => {
sinon.stub(element, 'changeChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -1893,7 +1735,7 @@
});
test('commit sha is populated from getChangeDetail', async () => {
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -2023,7 +1865,7 @@
test('revert dialog opened with revert param', async () => {
const awaitPluginsLoadedStub = sinon
- .stub(getPluginLoader(), 'awaitPluginsLoaded')
+ .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
.callsFake(() => Promise.resolve());
element.patchRange = {
@@ -2085,6 +1927,15 @@
});
test('reply from comment adds quote text', async () => {
+ const change = {
+ ...createChangeViewChange(),
+ revisions: createRevisions(1),
+ messages: createChangeMessages(1),
+ };
+ changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change,
+ });
const e = new CustomEvent('', {
detail: {message: {message: 'quote text'}},
});
@@ -2263,7 +2114,7 @@
test('selectedRevision updates when patchNum is changed', async () => {
const revision1: RevisionInfo = createRevision(1);
const revision2: RevisionInfo = createRevision(2);
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -2276,7 +2127,7 @@
current_revision: 'bbb' as CommitId,
},
});
- element.userModel.setPreferences(createPreferences());
+ userModel.setPreferences(createPreferences());
element.patchRange = {patchNum: 2 as RevisionPatchSetNum};
await element.performPostChangeLoadTasks();
@@ -2291,7 +2142,7 @@
const revision1 = createRevision(1);
const revision2 = createRevision(2);
const revision3 = createEditRevision();
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -2317,13 +2168,15 @@
element.change = {...change};
element.patchRange = {patchNum: 4 as RevisionPatchSetNum};
element.mergeable = true;
- const showStub = sinon.stub(element.jsAPI, 'handleEvent');
+ const showStub = sinon.stub(
+ testResolver(pluginLoaderToken).jsApiService,
+ 'handleShowChange'
+ );
element.sendShowChangeEvent();
assert.isTrue(showStub.calledOnce);
- assert.equal(showStub.lastCall.args[0], EventType.SHOW_CHANGE);
- assert.deepEqual(showStub.lastCall.args[1], {
+ assert.deepEqual(showStub.lastCall.args[0], {
change,
- patchNum: 4,
+ patchNum: 4 as PatchSetNumber,
info: {mergeable: true},
});
});
@@ -2563,9 +2416,9 @@
element.viewState = {
...createChangeViewState(),
changeNum: TEST_NUMERIC_CHANGE_ID,
- project: TEST_PROJECT_NAME,
+ repo: TEST_PROJECT_NAME,
};
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index 5f4835a..b238d77 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -10,9 +10,10 @@
import {customElement, state} from 'lit/decorators.js';
import {ChangeInfo, CommitId} from '../../../types/common';
import {fire, fireAlert} from '../../../utils/event-util';
-import {getAppContext} from '../../../services/app-context';
import {sharedStyles} from '../../../styles/shared-styles';
import {BindValueChangeEvent} from '../../../types/events';
+import {resolve} from '../../../models/dependency';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.';
const CHANGE_SUBJECT_LIMIT = 50;
@@ -73,6 +74,8 @@
@state()
private revertMessages: string[] = [];
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
static override styles = [
sharedStyles,
css`
@@ -170,8 +173,6 @@
`;
}
- private readonly jsAPI = getAppContext().jsApiService;
-
private computeIfSingleRevert() {
return this.revertType === RevertType.REVERT_SINGLE_CHANGE;
}
@@ -181,7 +182,11 @@
}
modifyRevertMsg(change: ChangeInfo, commitMessage: string, message: string) {
- return this.jsAPI.modifyRevertMsg(change, message, commitMessage);
+ return this.getPluginLoader().jsApiService.modifyRevertMsg(
+ change,
+ message,
+ commitMessage
+ );
}
populate(change: ChangeInfo, commitMessage: string, changes: ChangeInfo[]) {
@@ -231,7 +236,11 @@
msg: string,
commitMessage: string
) {
- return this.jsAPI.modifyRevertSubmissionMsg(change, msg, commitMessage);
+ return this.getPluginLoader().jsApiService.modifyRevertSubmissionMsg(
+ change,
+ msg,
+ commitMessage
+ );
}
populateRevertSubmissionMessage(
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 832738b..9bac7c2 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
@@ -38,10 +38,10 @@
shortcutsServiceToken,
} from '../../../services/shortcuts/shortcuts-service';
import {resolve} from '../../../models/dependency';
-import {getAppContext} from '../../../services/app-context';
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-file-list-header')
export class GrFileListHeader extends LitElement {
@@ -123,7 +123,7 @@
// 'hide diffs' buttons still be functional.
private readonly maxFilesForBulkActions = 225;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -131,7 +131,7 @@
super();
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
this.diffPrefs = diffPreferences;
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 7b79893..48cef64 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
@@ -54,6 +54,7 @@
></gr-file-list-header>`
);
element.diffPrefs = createDefaultDiffPrefs();
+ await element.updateComplete;
});
test('render', () => {
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 3e8530c..00d5a8c 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
@@ -21,8 +21,6 @@
import {FilesExpandedState} from '../gr-file-list-constants';
import {diffFilePaths, pluralize} from '../../../utils/string-util';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {getAppContext} from '../../../services/app-context';
import {
DiffViewMode,
@@ -61,7 +59,14 @@
import {changeModelToken} from '../../../models/change/change-model';
import {filesModelToken} from '../../../models/change/files-model';
import {ShortcutController} from '../../lit/shortcut-controller';
-import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {
+ css,
+ html,
+ LitElement,
+ nothing,
+ PropertyValues,
+ TemplateResult,
+} from 'lit';
import {Shortcut} from '../../../services/shortcuts/shortcuts-config';
import {fire} from '../../../utils/event-util';
import {a11yStyles} from '../../../styles/gr-a11y-styles';
@@ -76,6 +81,9 @@
import {createDiffUrl} from '../../../models/views/diff';
import {createEditUrl} from '../../../models/views/edit';
import {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';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -285,7 +293,9 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
@@ -589,6 +599,16 @@
top: 2px;
display: block;
}
+ .file-mode-warning {
+ font-size: 16px;
+ position: relative;
+ top: 2px;
+ color: var(--warning-foreground);
+ }
+ .file-mode-content {
+ display: inline-block;
+ color: var(--deemphasized-text-color);
+ }
@media screen and (max-width: 1200px) {
gr-endpoint-decorator.extra-col {
@@ -766,7 +786,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
this.diffPrefs = diffPreferences;
}
@@ -775,7 +795,7 @@
this,
() =>
select(
- this.userModel.preferences$,
+ this.getUserModel().preferences$,
prefs => !!prefs?.size_bar_in_change_table
),
sizeBarInChangeTable => {
@@ -784,7 +804,7 @@
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => {
this.loggedIn = loggedIn;
}
@@ -822,26 +842,29 @@
override connectedCallback() {
super.connectedCallback();
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
- this.dynamicHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
- 'change-view-file-list-header'
- );
- this.dynamicContentEndpoints = getPluginEndpoints().getDynamicEndpoints(
- 'change-view-file-list-content'
- );
+ this.dynamicHeaderEndpoints =
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-view-file-list-header'
+ );
+ this.dynamicContentEndpoints =
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-view-file-list-content'
+ );
this.dynamicPrependedHeaderEndpoints =
- getPluginEndpoints().getDynamicEndpoints(
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
'change-view-file-list-header-prepend'
);
this.dynamicPrependedContentEndpoints =
- getPluginEndpoints().getDynamicEndpoints(
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
'change-view-file-list-content-prepend'
);
- this.dynamicSummaryEndpoints = getPluginEndpoints().getDynamicEndpoints(
- 'change-view-file-list-summary'
- );
+ this.dynamicSummaryEndpoints =
+ this.getPluginLoader().pluginEndPoints.getDynamicEndpoints(
+ 'change-view-file-list-summary'
+ );
if (
this.dynamicHeaderEndpoints.length !==
@@ -1028,6 +1051,7 @@
data-file=${JSON.stringify(patchSetFile)}
tabindex="-1"
role="row"
+ aria-label=${file.__path}
>
<!-- endpoint: change-view-file-list-content-prepend -->
${when(showPrependedDynamicColumns, () =>
@@ -1099,10 +1123,14 @@
</div>`;
}
- private renderDivWithTooltip(content: string, tooltip: string) {
+ private renderDivWithTooltip(
+ content: TemplateResult | string,
+ tooltip: string,
+ cssClass = 'content'
+ ) {
return html`
<gr-tooltip-content title=${tooltip} has-tooltip>
- <div class="content">${content}</div>
+ <div class=${cssClass}>${content}</div>
</gr-tooltip-content>
`;
}
@@ -1142,12 +1170,18 @@
private renderFileStatusLeft(path?: string) {
if (this.filesLeftBase.length === 0) return nothing;
+ const arrow = html`
+ <gr-icon
+ icon="arrow_right_alt"
+ class="file-status-arrow"
+ aria-label="then"
+ ></gr-icon>
+ `;
// no path means "header row"
const psNum = this.patchRange?.basePatchNum;
if (!path) {
return html`
- ${this.renderDivWithTooltip(`${psNum}`, `Patchset ${psNum}`)}
- <gr-icon icon="arrow_right_alt" class="file-status-arrow"></gr-icon>
+ ${this.renderDivWithTooltip(`${psNum}`, `Patchset ${psNum}`)} ${arrow}
`;
}
if (isMagicPath(path)) return nothing;
@@ -1164,7 +1198,7 @@
.status=${status}
.labelPostfix=${postfix}
></gr-file-status>
- <gr-icon icon="arrow_right_alt" class="file-status-arrow"></gr-icon>
+ ${arrow}
`;
}
@@ -1181,6 +1215,7 @@
>
${computeTruncatedPath(file.__path)}
</span>
+ ${this.renderFileMode(file)}
<gr-copy-clipboard
?hideInput=${true}
.text=${file.__path}
@@ -1202,6 +1237,34 @@
`;
}
+ private renderFileMode(file: NormalizedFileInfo) {
+ const {old_mode, new_mode} = file;
+
+ // For added, modified or deleted regular files we do not want to render
+ // anything. Only if a file changed from something else to regular, then let
+ // the user know.
+ if (new_mode === undefined) return nothing;
+ let newModeStr = fileModeToString(new_mode, false);
+ if (new_mode === FileMode.REGULAR_FILE) {
+ if (old_mode === undefined) return nothing;
+ if (old_mode === FileMode.REGULAR_FILE) return nothing;
+ newModeStr = `non-${fileModeToString(old_mode, false)}`;
+ }
+
+ const changed = old_mode !== undefined && old_mode !== new_mode;
+ const icon = changed
+ ? html`<gr-icon icon="warning" class="file-mode-warning"></gr-icon> `
+ : '';
+ const action = changed
+ ? `changed from ${fileModeToString(old_mode)} to`
+ : 'is';
+ return this.renderDivWithTooltip(
+ html`${icon}(${newModeStr})`,
+ `file mode ${action} ${fileModeToString(new_mode)}`,
+ 'file-mode-content'
+ );
+ }
+
private renderStyledPath(filePath: string, previousFilePath?: string) {
const {matchingFolders, newFolders, fileName} = diffFilePaths(
filePath,
@@ -1256,10 +1319,7 @@
For example, without a nested div screen readers pronounce the
"Commit message" row content with incorrect column headers.
-->
- <div
- class=${this.computeSizeBarsClass(file.__path)}
- aria-label="A bar that represents the addition and deletion ratio for the current file"
- >
+ <div class=${this.computeSizeBarsClass(file.__path)} aria-hidden="true">
<svg width="61" height="8">
<rect
x=${this.computeBarAdditionX(file, sizeBarLayout)}
@@ -1291,7 +1351,7 @@
<span
class="added"
tabindex="0"
- aria-label=${`${file.lines_inserted} lines added`}
+ aria-label=${`${file.lines_inserted} added`}
?hidden=${file.binary}
>
+${file.lines_inserted}
@@ -1299,7 +1359,7 @@
<span
class="removed"
tabindex="0"
- aria-label=${`${file.lines_deleted} lines removed`}
+ aria-label=${`${file.lines_deleted} removed`}
?hidden=${file.binary}
>
-${file.lines_deleted}
@@ -1389,6 +1449,7 @@
}
private renderShowHide(file: NormalizedFileInfo) {
+ const expanded = this.isFileExpanded(file.__path);
return html` <div class="show-hide" role="gridcell">
<!-- Do not use input type="checkbox" with hidden input and
visible label here. Screen readers don't read/interract
@@ -1401,7 +1462,10 @@
role="switch"
tabindex="0"
aria-checked=${this.isFileExpandedStr(file.__path)}
- aria-label="Expand file"
+ aria-label=${expanded ? 'collapse' : 'expand'}
+ aria-description=${expanded
+ ? 'Collapse diff of this file'
+ : 'Expand diff of this file'}
@click=${this.expandedClick}
@keydown=${this.expandedClick}
>
@@ -1411,7 +1475,7 @@
class="show-hide-icon"
tabindex="-1"
id="icon"
- icon=${this.computeShowHideIcon(file.__path)}
+ icon=${expanded ? 'expand_less' : 'expand_more'}
></gr-icon>
</span>
</div>`;
@@ -2108,14 +2172,14 @@
if (this.editMode && path !== SpecialFilePath.MERGE_LIST) {
return createEditUrl({
changeNum: this.change._number,
- project: this.change.project,
+ repo: this.change.project,
path,
patchNum: this.patchRange.patchNum,
});
}
return createDiffUrl({
changeNum: this.change._number,
- project: this.change.project,
+ repo: this.change.project,
path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
@@ -2169,10 +2233,6 @@
return this.isFileExpanded(path) ? 'expanded' : '';
}
- private computeShowHideIcon(path: string | undefined) {
- return this.isFileExpanded(path) ? 'expand_less' : 'expand_more';
- }
-
private computeShowNumCleanlyMerged(): boolean {
return this.cleanlyMergedPaths.length > 0;
}
@@ -2595,7 +2655,7 @@
}
private handleReloadingDiffPreference() {
- this.userModel.getDiffPreferences();
+ this.getUserModel().getDiffPreferences();
}
private getOldPath(file: NormalizedFileInfo) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index a7d9e28..66f1e57 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -56,6 +56,7 @@
import {fixture, html, assert} from '@open-wc/testing';
import {Modifier} from '../../../utils/dom-util';
import {testResolver} from '../../../test/common-test-setup';
+import {FileMode} from '../../../utils/file-util';
suite('gr-diff a11y test', () => {
test('audit', async () => {
@@ -68,7 +69,7 @@
fileInfo: FileInfo = {}
): NormalizedFileInfo[] {
const files = Array(count).fill({});
- return files.map((_, idx) => normalize(fileInfo, `'/file${idx}`));
+ return files.map((_, idx) => normalize(fileInfo, `path/file${idx}`));
}
suite('gr-file-list tests', () => {
@@ -173,20 +174,23 @@
fileRows?.[0],
/* HTML */ `<div
class="file-row row"
- data-file='{"path":"'/file0"}'
+ data-file='{"path":"path/file0"}'
role="row"
tabindex="-1"
+ aria-label="path/file0"
>
<div class="status" role="gridcell">
<gr-file-status></gr-file-status>
</div>
<span class="path" role="gridcell">
<a class="pathLink">
- <span class="fullFileName" title="'/file0">
- <span class="newFilePath"> '/ </span>
+ <span class="fullFileName" title="path/file0">
+ <span class="newFilePath"> path/ </span>
<span class="fileName"> file0 </span>
</span>
- <span class="truncatedFileName" title="'/file0"> …/file0 </span>
+ <span class="truncatedFileName" title="path/file0">
+ …/file0
+ </span>
<gr-copy-clipboard hideinput=""> </gr-copy-clipboard>
</a>
</span>
@@ -201,17 +205,12 @@
</div>
</div>
<div class="desktop" role="gridcell">
- <div
- aria-label="A bar that represents the addition and deletion ratio for the current file"
- class="hide sizeBars"
- ></div>
+ <div aria-hidden="true" class="hide sizeBars"></div>
</div>
<div class="stats" role="gridcell">
<div>
- <span aria-label="9 lines added" class="added" tabindex="0">
- +9
- </span>
- <span aria-label="0 lines removed" class="removed" tabindex="0">
+ <span aria-label="9 added" class="added" tabindex="0"> +9 </span>
+ <span aria-label="0 removed" class="removed" tabindex="0">
-0
</span>
<span hidden=""> +/-0 B </span>
@@ -243,10 +242,11 @@
<div class="show-hide" role="gridcell">
<span
aria-checked="false"
- aria-label="Expand file"
+ aria-label="expand"
+ aria-description="Expand diff of this file"
class="show-hide"
data-expand="true"
- data-path="'/file0"
+ data-path="path/file0"
role="switch"
tabindex="0"
>
@@ -272,11 +272,13 @@
/* HTML */ `
<span class="path" role="gridcell">
<a class="pathLink">
- <span class="fullFileName" title="'/file0">
- <span class="newFilePath"> '/ </span>
+ <span class="fullFileName" title="path/file0">
+ <span class="newFilePath"> path/ </span>
<span class="fileName"> file0 </span>
</span>
- <span class="truncatedFileName" title="'/file0"> …/file0 </span>
+ <span class="truncatedFileName" title="path/file0">
+ …/file0
+ </span>
<gr-copy-clipboard hideinput=""> </gr-copy-clipboard>
</a>
</span>
@@ -288,11 +290,13 @@
/* HTML */ `
<span class="path" role="gridcell">
<a class="pathLink">
- <span class="fullFileName" title="'/file1">
- <span class="matchingFilePath"> '/ </span>
+ <span class="fullFileName" title="path/file1">
+ <span class="matchingFilePath"> path/ </span>
<span class="fileName"> file1 </span>
</span>
- <span class="truncatedFileName" title="'/file1"> …/file1 </span>
+ <span class="truncatedFileName" title="path/file1">
+ …/file1
+ </span>
<gr-copy-clipboard hideinput=""> </gr-copy-clipboard>
</a>
</span>
@@ -311,13 +315,55 @@
/* HTML */ `
<div class="extended status" role="gridcell">
<gr-file-status></gr-file-status>
- <gr-icon class="file-status-arrow" icon="arrow_right_alt"></gr-icon>
+ <gr-icon
+ aria-label="then"
+ class="file-status-arrow"
+ icon="arrow_right_alt"
+ ></gr-icon>
<gr-file-status></gr-file-status>
</div>
`
);
});
+ test('renders file mode', async () => {
+ element.files = createFiles(1, {
+ old_mode: FileMode.REGULAR_FILE,
+ new_mode: FileMode.EXECUTABLE_FILE,
+ });
+ await element.updateComplete;
+ const fileRows = queryAll<HTMLDivElement>(element, '.file-row');
+ const fileMode = queryAndAssert(
+ fileRows?.[0],
+ '.path gr-tooltip-content'
+ );
+ assert.dom.equal(
+ fileMode,
+ /* HTML */ `
+ <gr-tooltip-content
+ has-tooltip=""
+ title="file mode changed from regular (100644) to executable (100755)"
+ >
+ <div class="file-mode-content">
+ <gr-icon class="file-mode-warning" icon="warning"> </gr-icon>
+ (executable)
+ </div>
+ </gr-tooltip-content>
+ `
+ );
+ });
+
+ test('renders file mode, but not for regular files', async () => {
+ element.files = createFiles(3, {
+ old_mode: FileMode.REGULAR_FILE,
+ new_mode: FileMode.REGULAR_FILE,
+ });
+ await element.updateComplete;
+ const fileRows = queryAll<HTMLDivElement>(element, '.file-row');
+ const fileMode = query(fileRows?.[0], '.path gr-tooltip-content');
+ assert.notOk(fileMode);
+ });
+
test('renders file status column header', async () => {
element.files = createFiles(1, {lines_inserted: 9});
element.filesLeftBase = createFiles(1, {lines_inserted: 9});
@@ -332,7 +378,11 @@
<gr-tooltip-content has-tooltip="" title="Patchset 1">
<div class="content">1</div>
</gr-tooltip-content>
- <gr-icon class="file-status-arrow" icon="arrow_right_alt"></gr-icon>
+ <gr-icon
+ aria-label="then"
+ class="file-status-arrow"
+ icon="arrow_right_alt"
+ ></gr-icon>
<gr-tooltip-content has-tooltip="" title="Patchset 2">
<div class="content">2</div>
</gr-tooltip-content>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index 5417127..c46f4fc 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -322,8 +322,7 @@
@state()
private combinedMessages: CombinedMessage[] = [];
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly changeModel = resolve(this, changeModelToken);
@@ -465,15 +464,7 @@
await el.updateComplete;
await query<GrFormattedText>(el, 'gr-formatted-text.message')
?.updateComplete;
- let top = el.offsetTop;
- for (
- let offsetParent = el.offsetParent as HTMLElement | null;
- offsetParent;
- offsetParent = offsetParent.offsetParent as HTMLElement | null
- ) {
- top += offsetParent.offsetTop;
- }
- window.scrollTo(0, top);
+ el.scrollIntoView();
this.highlightEl(el);
}
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index 2e62718..84f3bf9 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -32,6 +32,8 @@
import {fixture, assert} from '@open-wc/testing';
import {GrButton} from '../../shared/gr-button/gr-button';
import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {commentsModelToken} from '../../../models/comments/comments-model';
const author = {
_account_id: 42 as AccountId,
@@ -136,7 +138,9 @@
element = await fixture<GrMessagesList>(
html`<gr-messages-list></gr-messages-list>`
);
- await element.getCommentsModel().reloadComments(0 as NumericChangeId);
+ await testResolver(commentsModelToken).reloadComments(
+ 0 as NumericChangeId
+ );
element.messages = messages;
await element.updateComplete;
});
@@ -229,7 +233,6 @@
message.message = {...message.message, expanded: false};
}
- const scrollToStub = sinon.stub(window, 'scrollTo');
const highlightStub = sinon.stub(element, 'highlightEl');
await element.scrollToMessage('invalid');
@@ -243,6 +246,11 @@
}
const messageID = messages[1].id;
+
+ const selector = `[data-message-id="${messageID}"]`;
+ const el = queryAndAssert<GrMessage>(element, selector);
+ const scrollToStub = sinon.stub(el, 'scrollIntoView');
+
await element.scrollToMessage(messageID);
assert.isTrue(
queryAndAssert<GrMessage>(element, `[data-message-id="${messageID}"]`)
@@ -254,14 +262,18 @@
});
test('scroll to message offscreen', async () => {
- const scrollToStub = sinon.stub(window, 'scrollTo');
const highlightStub = sinon.stub(element, 'highlightEl');
element.messages = generateRandomMessages(25);
await element.updateComplete;
- assert.isFalse(scrollToStub.called);
assert.isFalse(highlightStub.called);
const messageID = element.messages[1].id;
+ const selector = `[data-message-id="${messageID}"]`;
+ const el = queryAndAssert<GrMessage>(element, selector);
+ const scrollToStub = sinon.stub(el, 'scrollIntoView');
+
+ assert.isFalse(scrollToStub.called);
+
await element.scrollToMessage(messageID);
assert.isTrue(scrollToStub.calledOnce);
assert.isTrue(highlightStub.calledOnce);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 11fe0e2..bdb95ea 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -225,7 +225,7 @@
.href=${change?._change_number
? createChangeUrl({
changeNum: change._change_number,
- project: change.project,
+ repo: change.project,
usp: 'related-change',
patchNum: change._revision_number as RevisionPatchSetNum,
})
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index dcc6039..676a468 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -9,6 +9,7 @@
import {ChangeStatus} from '../../../constants/constants';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import '../../../test/common-test-setup';
+import {testResolver} from '../../../test/common-test-setup';
import {
createChange,
createCommitInfoWithRequiredCommit,
@@ -21,7 +22,6 @@
import {
query,
queryAndAssert,
- resetPlugins,
stubRestApi,
waitEventLoop,
} from '../../../test/test-utils';
@@ -38,7 +38,7 @@
import {ParsedChangeInfo} from '../../../types/types';
import {getChangeNumber} from '../../../utils/change-util';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import './gr-related-changes-list';
import {
ChangeMarkersInList,
@@ -646,16 +646,11 @@
let element: GrRelatedChangesList;
setup(async () => {
- resetPlugins();
element = await fixture(
html`<gr-related-changes-list></gr-related-changes-list>`
);
});
- teardown(() => {
- resetPlugins();
- });
-
test('endpoint params', async () => {
element.change = {...createParsedChange(), labels: {}};
interface RelatedChangesListGrEndpointDecorator
@@ -676,7 +671,7 @@
'0.1',
'http://some/plugins/url1.js'
);
- getPluginLoader().loadPlugins([]);
+ testResolver(pluginLoaderToken).loadPlugins([]);
await waitEventLoop();
assert.strictEqual(hookEl!.plugin, plugin!);
assert.strictEqual(hookEl!.change, element.change);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
index 6fba4e4..25e3e51 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
@@ -7,11 +7,10 @@
import './gr-reply-dialog';
import {
queryAndAssert,
- resetPlugins,
stubRestApi,
waitEventLoop,
} from '../../../test/test-utils';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+
import {GrReplyDialog} from './gr-reply-dialog';
import {fixture, html, assert} from '@open-wc/testing';
import {
@@ -22,6 +21,8 @@
} from '../../../types/common';
import {createChange} from '../../../test/test-data-generators';
import {GrButton} from '../../shared/gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-reply-dialog-it tests', () => {
let element: GrReplyDialog;
@@ -80,10 +81,6 @@
await element.updateComplete;
});
- teardown(() => {
- resetPlugins();
- });
-
test('submit blocked when invalid email is supplied to ccs', async () => {
const sendStub = sinon.stub(element, 'send').returns(Promise.resolve());
@@ -99,7 +96,6 @@
});
test('lgtm plugin', async () => {
- resetPlugins();
window.Gerrit.install(
plugin => {
const replyApi = plugin.changeReply();
@@ -116,8 +112,9 @@
);
element = await fixture(html`<gr-reply-dialog></gr-reply-dialog>`);
setupElement(element);
- getPluginLoader().loadPlugins([]);
- await getPluginLoader().awaitPluginsLoaded();
+ const pluginLoader = testResolver(pluginLoaderToken);
+ pluginLoader.loadPlugins([]);
+ await pluginLoader.awaitPluginsLoaded();
await waitEventLoop();
await waitEventLoop();
const labelScoreRows = queryAndAssert(
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index cd42a9f..28d65f4 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -15,7 +15,7 @@
import '../gr-label-scores/gr-label-scores';
import '../gr-thread-list/gr-thread-list';
import '../../../styles/shared-styles';
-import {GrReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {GrReviewerSuggestionsProvider} from '../../../services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import {getAppContext} from '../../../services/app-context';
import {
ChangeStatus,
@@ -33,7 +33,7 @@
import {TargetElement} from '../../../api/plugin';
import {
FixIronA11yAnnouncer,
- notUndefined,
+ isDefined,
ParsedChangeInfo,
} from '../../../types/types';
import {
@@ -129,8 +129,11 @@
GrComment,
} from '../../shared/gr-comment/gr-comment';
import {ShortcutController} from '../../lit/shortcut-controller';
-import {Key, Modifier} from '../../../utils/dom-util';
+import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
import {GrThreadList} from '../gr-thread-list/gr-thread-list';
+import {userModelToken} from '../../../models/user/user-model';
+import {accountsModelToken} from '../../../models/accounts-model/accounts-model';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
export enum FocusTarget {
ANY = 'any',
@@ -179,13 +182,6 @@
*/
/**
- * Fired when the main textarea's value changes, which may have triggered
- * a change in size for the dialog.
- *
- * @event autogrow
- */
-
- /**
* Fires to show an alert when a send is attempted on the non-latest patch.
*
* @event show-alert
@@ -216,8 +212,7 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
// TODO: update type to only ParsedChangeInfo
@property({type: Object})
@@ -241,6 +236,8 @@
@property({type: Object})
projectConfig?: ConfigInfo;
+ @query('#patchsetLevelComment') patchsetLevelGrComment?: GrComment;
+
@query('#reviewers') reviewersList?: GrAccountList;
@query('#ccs') ccsList?: GrAccountList;
@@ -387,13 +384,15 @@
private readonly restApiService: RestApiService =
getAppContext().restApiService;
- private readonly jsAPI = getAppContext().jsApiService;
-
private readonly flagsService = getAppContext().flagsService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
private readonly getConfigModel = resolve(this, configModelToken);
- private readonly accountsModel = getAppContext().accountsModel;
+ private readonly getAccountsModel = resolve(this, accountsModelToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private latestPatchNum?: PatchSetNumber;
@@ -615,7 +614,6 @@
this.filterReviewerSuggestion =
this.filterReviewerSuggestionGenerator(false);
this.filterCCSuggestion = this.filterReviewerSuggestionGenerator(true);
- this.jsAPI.addElement(TargetElement.REPLY_DIALOG, this);
this.shortcuts.addLocal({key: Key.ESC}, () => this.cancel());
this.shortcuts.addLocal(
@@ -629,7 +627,7 @@
subscribe(
this,
- () => getAppContext().userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
subscribe(
@@ -690,6 +688,12 @@
(
IronA11yAnnouncer as unknown as FixIronA11yAnnouncer
).requestAvailability();
+
+ this.getPluginLoader().jsApiService.addElement(
+ TargetElement.REPLY_DIALOG,
+ this
+ );
+
this.restApiService.getAccount().then(account => {
if (account) this.account = account;
});
@@ -743,12 +747,6 @@
this.computeMessagePlaceholder();
this.computeSendButtonLabel();
}
- if (changedProperties.has('reviewFormatting')) {
- this.handleHeightChanged();
- }
- if (changedProperties.has('draftCommentThreads')) {
- this.handleHeightChanged();
- }
if (changedProperties.has('sendDisabled')) {
this.sendDisabledChanged();
}
@@ -1411,13 +1409,15 @@
)
.filter(user => !this.currentAttentionSet.has(user))
.map(user => allAccounts.find(a => getUserId(a) === user))
- .filter(notUndefined);
+ .filter(isDefined);
const newAttentionSetUsers = (
await Promise.all(
- newAttentionSetAdditions.map(a => this.accountsModel.fillDetails(a))
+ newAttentionSetAdditions.map(a =>
+ this.getAccountsModel().fillDetails(a)
+ )
)
- ).filter(notUndefined);
+ ).filter(isDefined);
for (const user of newAttentionSetUsers) {
let reason;
@@ -1446,11 +1446,7 @@
reviewInput.remove_from_attention_set
);
- const patchsetLevelComment = queryAndAssert<GrComment>(
- this,
- '#patchsetLevelComment'
- );
- await patchsetLevelComment.save();
+ await this.patchsetLevelGrComment?.save();
assertIsDefined(this.change, 'change');
reviewInput.reviewers = this.computeReviewers();
@@ -1494,13 +1490,17 @@
if (!section || section === FocusTarget.ANY) {
section = this.chooseFocusTarget();
}
- if (section === FocusTarget.REVIEWERS) {
- const reviewerEntry = this.reviewersList?.focusStart;
- setTimeout(() => reviewerEntry?.focus());
- } else if (section === FocusTarget.CCS) {
- const ccEntry = this.ccsList?.focusStart;
- setTimeout(() => ccEntry?.focus());
- }
+ whenVisible(this, () => {
+ if (section === FocusTarget.REVIEWERS) {
+ const reviewerEntry = this.reviewersList?.focusStart;
+ reviewerEntry?.focus();
+ } else if (section === FocusTarget.CCS) {
+ const ccEntry = this.ccsList?.focusStart;
+ ccEntry?.focus();
+ } else {
+ this.patchsetLevelGrComment?.focus();
+ }
+ });
}
chooseFocusTarget() {
@@ -1860,11 +1860,7 @@
bubbles: false,
})
);
- const patchsetLevelComment = queryAndAssert<GrComment>(
- this,
- '#patchsetLevelComment'
- );
- await patchsetLevelComment.save();
+ await this.patchsetLevelGrComment?.save();
this.rebuildReviewerArrays();
}
@@ -1973,10 +1969,6 @@
);
}
- handleHeightChanged() {
- fireEvent(this, 'autogrow');
- }
-
getLabelScores(): GrLabelScores {
return this.labelScores || queryAndAssert(this, 'gr-label-scores');
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index acd1755..4ad76f4 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -15,6 +15,7 @@
queryAndAssert,
stubFlags,
stubRestApi,
+ waitUntilVisible,
} from '../../../test/test-utils';
import {ChangeStatus, ReviewerState} from '../../../constants/constants';
import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
@@ -62,6 +63,11 @@
import {KnownExperimentId} from '../../../services/flags/flags';
import {Key, Modifier} from '../../../utils/dom-util';
import {GrComment} from '../../shared/gr-comment/gr-comment';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
function cloneableResponse(status: number, text: string) {
return {
@@ -87,6 +93,7 @@
let element: GrReplyDialog;
let changeNum: NumericChangeId;
let patchNum: PatchSetNum;
+ let commentsModel: CommentsModel;
let lastId = 1;
const makeAccount = function () {
@@ -147,6 +154,7 @@
element.draftCommentThreads = [];
await element.updateComplete;
+ commentsModel = testResolver(commentsModelToken);
});
function stubSaveReview(
@@ -1444,14 +1452,10 @@
test('focusOn', async () => {
await element.updateComplete;
- const clock = sinon.useFakeTimers();
const chooseFocusTargetSpy = sinon.spy(element, 'chooseFocusTarget');
element.focusOn();
- // element.focus() is called after a setTimeout(). The focusOn() method
- // does not trigger any changes in the element hence element.updateComplete
- // resolves immediately and cannot be used here, hence tick the clock here
- // explicitly instead
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 1);
assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-COMMENT');
assert.equal(
@@ -1460,7 +1464,8 @@
);
element.focusOn(element.FocusTarget.ANY);
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 2);
assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-COMMENT');
assert.equal(
@@ -1469,7 +1474,8 @@
);
element.focusOn(element.FocusTarget.BODY);
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 2);
assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-COMMENT');
assert.equal(
@@ -1478,23 +1484,21 @@
);
element.focusOn(element.FocusTarget.REVIEWERS);
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 2);
- assert.equal(
- element?.shadowRoot?.activeElement?.tagName,
- 'GR-ACCOUNT-LIST'
+ await waitUntil(
+ () => element?.shadowRoot?.activeElement?.tagName === 'GR-ACCOUNT-LIST'
);
assert.equal(element?.shadowRoot?.activeElement?.id, 'reviewers');
element.focusOn(element.FocusTarget.CCS);
- clock.tick(1);
assert.equal(chooseFocusTargetSpy.callCount, 2);
assert.equal(
element?.shadowRoot?.activeElement?.tagName,
'GR-ACCOUNT-LIST'
);
- assert.equal(element?.shadowRoot?.activeElement?.id, 'ccs');
- clock.restore();
+ await waitUntil(() => element?.shadowRoot?.activeElement?.id === 'ccs');
});
test('chooseFocusTarget', () => {
@@ -2018,17 +2022,6 @@
await promise;
});
- test('fires height change when the drafts comments load', async () => {
- // Flush DOM operations before binding to the autogrow event so we don't
- // catch the events fired from the initial layout.
- await element.updateComplete;
- const autoGrowHandler = sinon.stub();
- element.addEventListener('autogrow', autoGrowHandler);
- element.draftCommentThreads = [];
- await element.updateComplete;
- assert.isTrue(autoGrowHandler.called);
- });
-
suite('start review and save buttons', () => {
let sendStub: sinon.SinonStub;
@@ -2381,7 +2374,7 @@
test('replies to patchset level comments are not filtered out', async () => {
const draft = {...createDraft(), in_reply_to: '1' as UrlEncodedCommentId};
- element.getCommentsModel().setState({
+ commentsModel.setState({
drafts: {
'abc.txt': [draft],
},
@@ -2417,7 +2410,7 @@
...createDraft(),
message: 'hey @abcd@def take a look at this',
};
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2452,7 +2445,7 @@
message: 'hey @abcd@def.com take a look at this',
unresolved: true,
};
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2492,7 +2485,7 @@
message: 'hey @abcd@def.com take a look at this',
unresolved: true,
};
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2545,7 +2538,7 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2582,7 +2575,7 @@
registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 27b5097..414fed9 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -44,6 +44,7 @@
import {Interaction} from '../../../constants/reporting';
import {KnownExperimentId} from '../../../services/flags/flags';
import {HtmlPatched} from '../../../utils/lit-util';
+import {userModelToken} from '../../../models/user/user-model';
enum SortDropdownState {
TIMESTAMP = 'Latest timestamp',
@@ -205,7 +206,7 @@
private readonly flagsService = getAppContext().flagsService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly patched = new HtmlPatched(key => {
this.reporting.reportInteraction(Interaction.AUTOCLOSE_HTML_PATCHED, {
@@ -228,7 +229,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
// for COMMENTS_AUTOCLOSE logging purposes only
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 848948f..f7e3542 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -72,7 +72,6 @@
import {changeModelToken} from '../../models/change/change-model';
import {getAppContext} from '../../services/app-context';
import {when} from 'lit/directives/when.js';
-import {KnownExperimentId} from '../../services/flags/flags';
import {HtmlPatched} from '../../utils/lit-util';
import {DropdownItem} from '../shared/gr-dropdown-list/gr-dropdown-list';
import './gr-checks-attempt';
@@ -125,8 +124,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flags = getAppContext().flagsService;
-
constructor() {
super();
subscribe(
@@ -536,10 +533,8 @@
private renderActions() {
const actions = [...(this.result?.actions ?? [])];
- if (this.flags.isEnabled(KnownExperimentId.CHECKS_FIXES)) {
- const fixAction = createFixAction(this, this.result);
- if (fixAction) actions.unshift(fixAction);
- }
+ const fixAction = createFixAction(this, this.result);
+ if (fixAction) actions.unshift(fixAction);
if (actions.length === 0) return;
const overflowItems = actions.slice(2).map(action => {
return {...action, id: action.name};
@@ -711,7 +706,7 @@
tooltip: `${path}${rangeText}`,
url: createDiffUrl({
changeNum: change._number,
- project: change.project,
+ repo: change.project,
path,
patchNum: patchset,
lineNum: line,
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
index 0aca71f..0a21da4 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
@@ -13,8 +13,6 @@
import './gr-checks-results';
import './gr-hovercard-run';
import {fontStyles} from '../../styles/gr-font-styles';
-import {KnownExperimentId} from '../../services/flags/flags';
-import {getAppContext} from '../../services/app-context';
@customElement('gr-diff-check-result')
export class GrDiffCheckResult extends LitElement {
@@ -34,8 +32,6 @@
@state()
isExpandable = false;
- private readonly flags = getAppContext().flagsService;
-
static override get styles() {
return [
fontStyles,
@@ -190,7 +186,6 @@
}
private renderFixButton() {
- if (!this.flags.isEnabled(KnownExperimentId.CHECKS_FIXES)) return nothing;
const action = createFixAction(this, this.result);
if (!action) return nothing;
return html`
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index e663ae1..e873b82 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -3,8 +3,6 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-/* Import to get Gerrit interface */
-/* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */
import '../gr-error-dialog/gr-error-dialog';
import '../../shared/gr-alert/gr-alert';
import '../../shared/gr-overlay/gr-overlay';
@@ -17,6 +15,7 @@
import {ErrorType, FixIronA11yAnnouncer} from '../../../types/types';
import {AccountId} from '../../../types/common';
import {
+ AuthErrorEvent,
EventType,
NetworkErrorEvent,
ServerErrorEvent,
@@ -28,6 +27,8 @@
import {fireIronAnnounce} from '../../../utils/event-util';
import {LitElement, html} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
+import {authServiceToken} from '../../../services/gr-auth/gr-auth';
+import {resolve} from '../../../models/dependency';
const HIDE_ALERT_TIMEOUT_MS = 10 * 1000;
const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -116,11 +117,7 @@
private readonly reporting = getAppContext().reportingService;
- private readonly _authService = getAppContext().authService;
-
- private readonly eventEmitter = getAppContext().eventEmitter;
-
- private authErrorHandlerDeregistrationHook?: Function;
+ private readonly getAuthService = resolve(this, authServiceToken);
private readonly restApiService = getAppContext().restApiService;
@@ -135,13 +132,7 @@
document.addEventListener('show-error', this.handleShowErrorDialog);
document.addEventListener('visibilitychange', this.handleVisibilityChange);
document.addEventListener('show-auth-required', this.handleAuthRequired);
-
- this.authErrorHandlerDeregistrationHook = this.eventEmitter.on(
- 'auth-error',
- event => {
- this.handleAuthError(event.message, event.action);
- }
- );
+ document.addEventListener('auth-error', this.handleAuthError);
(
IronA11yAnnouncer as unknown as FixIronA11yAnnouncer
@@ -168,9 +159,7 @@
document.removeEventListener('show-auth-required', this.handleAuthRequired);
this.checkLoggedInTask?.cancel();
- if (this.authErrorHandlerDeregistrationHook) {
- this.authErrorHandlerDeregistrationHook();
- }
+ document.removeEventListener('auth-error', this.handleAuthError);
super.disconnectedCallback();
}
@@ -205,11 +194,11 @@
);
};
- private handleAuthError(msg: string, action: string) {
+ private handleAuthError = (event: AuthErrorEvent) => {
this.noInteractionOverlay.open().then(() => {
- this.showAuthErrorAlert(msg, action);
+ this.showAuthErrorAlert(event.detail.message, event.detail.action);
});
- }
+ };
private readonly handleServerError = (e: ServerErrorEvent) => {
const {request, response} = e.detail;
@@ -218,7 +207,7 @@
const {status, statusText} = response;
if (
response.status === 403 &&
- !this._authService.isAuthed &&
+ !this.getAuthService().isAuthed &&
errorText === AUTHENTICATION_REQUIRED
) {
// if not authed previously, this is trying to access auth required APIs
@@ -226,13 +215,13 @@
this.handleAuthRequired();
} else if (
response.status === 403 &&
- this._authService.isAuthed &&
+ this.getAuthService().isAuthed &&
errorText === AUTHENTICATION_REQUIRED
) {
// The app was logged at one point and is now getting auth errors.
// This indicates the auth token may no longer valid.
// Re-check on auth
- this._authService.clearCache();
+ this.getAuthService().clearCache();
this.restApiService.getLoggedIn();
} else if (!this.shouldSuppressError(errorText)) {
const trace =
@@ -445,7 +434,7 @@
// force to refetch account info
this.restApiService.invalidateAccountsCache();
- this._authService.clearCache();
+ this.getAuthService().clearCache();
this.restApiService.getLoggedIn().then(isLoggedIn => {
if (!this.refreshingCredentials) return;
@@ -507,7 +496,7 @@
this.noInteractionOverlay.close();
// Clear the cache for auth
- this._authService.clearCache();
+ this.getAuthService().clearCache();
}
private readonly handleWindowFocus = () => {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index e0de507..3d808a9 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -11,7 +11,6 @@
__testOnly_ErrorType,
} from './gr-error-manager';
import {
- stubAuth,
stubReporting,
stubRestApi,
waitEventLoop,
@@ -26,6 +25,8 @@
import {fixture, assert} from '@open-wc/testing';
import {html} from 'lit';
import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {authServiceToken} from '../../../services/gr-auth/gr-auth';
suite('gr-error-manager tests', () => {
let element: GrErrorManager;
@@ -37,9 +38,9 @@
let appContext: AppContext;
setup(async () => {
- fetchStub = stubAuth('fetch').returns(
- Promise.resolve({...new Response(), ok: true, status: 204})
- );
+ fetchStub = sinon
+ .stub(testResolver(authServiceToken), 'fetch')
+ .returns(Promise.resolve({...new Response(), ok: true, status: 204}));
appContext = getAppContext();
getLoggedInStub = stubRestApi('getLoggedIn').callsFake(() =>
appContext.authService.authCheck()
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 dcc9a99..1c4e237 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
@@ -11,7 +11,6 @@
import '../gr-account-dropdown/gr-account-dropdown';
import '../gr-smart-search/gr-smart-search';
import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {getAdminLinks, NavLink} from '../../../utils/admin-nav-util';
import {
AccountDetailInfo,
@@ -29,6 +28,8 @@
import {fireEvent} from '../../../utils/event-util';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
+import {userModelToken} from '../../../models/user/user-model';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
@@ -140,11 +141,11 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly jsAPI = getAppContext().jsApiService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- private readonly configModel = resolve(this, configModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
private subscriptions: Subscription[] = [];
@@ -153,8 +154,8 @@
this.loadAccount();
this.subscriptions.push(
- this.userModel.preferences$
- .pipe(
+ this.getUserModel()
+ .preferences$.pipe(
map(preferences => preferences?.my ?? []),
distinctUntilChanged()
)
@@ -163,7 +164,7 @@
})
);
this.subscriptions.push(
- this.configModel().serverConfig$.subscribe(config => {
+ this.getConfigModel().serverConfig$.subscribe(config => {
if (!config) return;
this.serverConfig = config;
this.retrieveFeedbackURL(config);
@@ -570,7 +571,7 @@
return Promise.all([
this.restApiService.getAccount(),
this.restApiService.getTopMenus(),
- getPluginLoader().awaitPluginsLoaded(),
+ this.getPluginLoader().awaitPluginsLoaded(),
]).then(result => {
const account = result[0];
this.account = account;
@@ -587,7 +588,7 @@
}
return capabilities;
}),
- () => this.jsAPI.getAdminMenuLinks()
+ () => this.getPluginLoader().jsApiService.getAdminMenuLinks()
).then(res => {
this.adminLinks = res.links;
});
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 1102895..f547cc7 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -317,7 +317,15 @@
if (this.view !== GerritView.CHANGE) return;
const browserUrl = new URL(window.location.toString());
const stateUrl = new URL(createChangeUrl(state), browserUrl);
+
+ // Keeping the hash and certain parameters are stop-gap solution. We
+ // should find better ways of maintaining an overall consistent URL
+ // state.
stateUrl.hash = browserUrl.hash;
+ for (const p of browserUrl.searchParams.entries()) {
+ if (p[0] === 'experiment') stateUrl.searchParams.append(p[0], p[1]);
+ }
+
if (browserUrl.toString() !== stateUrl.toString()) {
page.replace(
stateUrl.toString(),
@@ -335,6 +343,7 @@
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
+ this.subscriptions = [];
}
start() {
@@ -345,12 +354,8 @@
}
setState(state: AppElementParams) {
- if (
- 'project' in state &&
- state.project !== undefined &&
- 'changeNum' in state
- )
- this.restApiService.setInProjectLookup(state.changeNum, state.project);
+ if ('repo' in state && state.repo !== undefined && 'changeNum' in state)
+ this.restApiService.setInProjectLookup(state.changeNum, state.repo);
this.routerModel.setState({
view: state.view,
@@ -1359,27 +1364,28 @@
}
handleQueryRoute(ctx: PageContext) {
- const state: SearchViewState = {
+ const state: Partial<SearchViewState> = {
view: GerritView.SEARCH,
query: ctx.params[0],
offset: ctx.params[2],
};
// Note that router model view must be updated before view models.
- this.setState(state);
- this.searchViewModel.setState(state);
+ this.setState(state as AppElementParams);
+ this.searchViewModel.updateState(state);
}
handleChangeIdQueryRoute(ctx: PageContext) {
// TODO(pcc): This will need to indicate that this was a change ID query if
// standard queries gain the ability to search places like commit messages
// for change IDs.
- const state: SearchViewState = {
+ const state: Partial<SearchViewState> = {
view: GerritView.SEARCH,
query: ctx.params[0],
+ offset: undefined,
};
// Note that router model view must be updated before view models.
- this.setState(state);
- this.searchViewModel.setState(state);
+ this.setState(state as AppElementParams);
+ this.searchViewModel.updateState(state);
}
handleQueryLegacySuffixRoute(ctx: PageContext) {
@@ -1394,7 +1400,7 @@
// Parameter order is based on the regex group number matched.
const changeNum = Number(ctx.params[1]) as NumericChangeId;
const state: ChangeViewState = {
- project: ctx.params[0] as RepoName,
+ repo: ctx.params[0] as RepoName,
changeNum,
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]) as RevisionPatchSetNum,
@@ -1420,8 +1426,8 @@
const selected = queryMap.get('checksRunsSelected');
if (selected) state.checksRunsSelected = new Set(selected.split(','));
- assertIsDefined(state.project, 'project');
- this.reporting.setRepoName(state.project);
+ assertIsDefined(state.repo, 'project');
+ this.reporting.setRepoName(state.repo);
this.reporting.setChangeId(changeNum);
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
@@ -1432,13 +1438,13 @@
handleCommentRoute(ctx: PageContext) {
const changeNum = Number(ctx.params[1]) as NumericChangeId;
const state: DiffViewState = {
- project: ctx.params[0] as RepoName,
+ repo: ctx.params[0] as RepoName,
changeNum,
commentId: ctx.params[2] as UrlEncodedCommentId,
view: GerritView.DIFF,
commentLink: true,
};
- this.reporting.setRepoName(state.project ?? '');
+ this.reporting.setRepoName(state.repo ?? '');
this.reporting.setChangeId(changeNum);
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
@@ -1449,13 +1455,13 @@
handleCommentsRoute(ctx: PageContext) {
const changeNum = Number(ctx.params[1]) as NumericChangeId;
const state: ChangeViewState = {
- project: ctx.params[0] as RepoName,
+ repo: ctx.params[0] as RepoName,
changeNum,
commentId: ctx.params[2] as UrlEncodedCommentId,
view: GerritView.CHANGE,
};
- assertIsDefined(state.project);
- this.reporting.setRepoName(state.project);
+ assertIsDefined(state.repo);
+ this.reporting.setRepoName(state.repo);
this.reporting.setChangeId(changeNum);
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
@@ -1467,7 +1473,7 @@
const changeNum = Number(ctx.params[1]) as NumericChangeId;
// Parameter order is based on the regex group number matched.
const state: DiffViewState = {
- project: ctx.params[0] as RepoName,
+ repo: ctx.params[0] as RepoName,
changeNum,
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]) as RevisionPatchSetNum,
@@ -1479,7 +1485,7 @@
state.leftSide = address.leftSide;
state.lineNum = address.lineNum;
}
- this.reporting.setRepoName(state.project ?? '');
+ this.reporting.setRepoName(state.repo ?? '');
this.reporting.setChangeId(changeNum);
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
@@ -1513,7 +1519,7 @@
const project = ctx.params[0] as RepoName;
const changeNum = Number(ctx.params[1]) as NumericChangeId;
const state: EditViewState = {
- project,
+ repo: project,
changeNum,
// for edit view params, patchNum cannot be undefined
patchNum: convertToPatchSetNum(ctx.params[2]) as RevisionPatchSetNum,
@@ -1535,7 +1541,7 @@
const changeNum = Number(ctx.params[1]) as NumericChangeId;
const queryMap = new URLSearchParams(ctx.querystring);
const state: ChangeViewState = {
- project,
+ repo: project,
changeNum,
patchNum: convertToPatchSetNum(ctx.params[3]) as RevisionPatchSetNum,
view: GerritView.CHANGE,
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 854222b..597c7c0 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
@@ -31,15 +31,13 @@
import {EditViewState} from '../../../models/views/edit';
import {ChangeViewState} from '../../../models/views/change';
import {PatchRangeParams} from '../../../utils/url-util';
-import {DependencyRequestEvent} from '../../../models/dependency';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-router tests', () => {
let router: GrRouter;
setup(() => {
- document.dispatchEvent(
- new DependencyRequestEvent(routerToken, x => (router = x))
- );
+ router = testResolver(routerToken);
});
test('getHashFromCanonicalPath', () => {
@@ -403,7 +401,7 @@
view: GerritView.SEARCH,
query: 'project:foo/bar/baz',
offset: undefined,
- });
+ } as AppElementParams);
ctx.params[1] = '123';
ctx.params[2] = '123';
@@ -411,7 +409,7 @@
view: GerritView.SEARCH,
query: 'project:foo/bar/baz',
offset: '123',
- });
+ } as AppElementParams);
});
test('handleQueryLegacySuffixRoute', () => {
@@ -429,7 +427,8 @@
assertctxToParams(ctx, 'handleChangeIdQueryRoute', {
view: GerritView.SEARCH,
query: 'I0123456789abcdef0123456789abcdef01234567',
- });
+ offset: undefined,
+ } as AppElementParams);
});
suite('handleRegisterRoute', () => {
@@ -1134,7 +1133,7 @@
const ctx = makeParams('', '');
assertctxToParams(ctx, 'handleChangeRoute', {
view: GerritView.CHANGE,
- project: 'foo/bar' as RepoName,
+ repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
patchNum: 7 as RevisionPatchSetNum,
@@ -1154,7 +1153,7 @@
ctx.querystring = queryMap.toString();
assertctxToParams(ctx, 'handleChangeRoute', {
view: GerritView.CHANGE,
- project: 'foo/bar' as RepoName,
+ repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
patchNum: 7 as RevisionPatchSetNum,
@@ -1194,7 +1193,7 @@
const ctx = makeParams('foo/bar/baz', 'b44');
assertctxToParams(ctx, 'handleDiffRoute', {
view: GerritView.DIFF,
- project: 'foo/bar' as RepoName,
+ repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
patchNum: 7 as RevisionPatchSetNum,
@@ -1217,7 +1216,7 @@
{params: groups!.slice(1)} as any,
'handleCommentRoute',
{
- project: 'gerrit' as RepoName,
+ repo: 'gerrit' as RepoName,
changeNum: 264833 as NumericChangeId,
commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
commentLink: true,
@@ -1238,7 +1237,7 @@
{params: groups!.slice(1)} as any,
'handleCommentsRoute',
{
- project: 'gerrit' as RepoName,
+ repo: 'gerrit' as RepoName,
changeNum: 264833 as NumericChangeId,
commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
view: GerritView.CHANGE,
@@ -1260,7 +1259,7 @@
},
};
const appParams: EditViewState = {
- project: 'foo/bar' as RepoName,
+ repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.EDIT,
path: 'foo/bar/baz',
@@ -1286,7 +1285,7 @@
},
};
const appParams: EditViewState = {
- project: 'foo/bar' as RepoName,
+ repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.EDIT,
path: 'foo/bar/baz',
@@ -1311,7 +1310,7 @@
},
};
const appParams: ChangeViewState = {
- project: 'foo/bar' as RepoName,
+ repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.CHANGE,
patchNum: 3 as RevisionPatchSetNum,
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index dbb3db9..01e2fb6 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -6,7 +6,7 @@
import '../../../test/common-test-setup';
import './gr-search-bar';
import {GrSearchBar} from './gr-search-bar';
-import '../../../scripts/util';
+import '../../../utils/async-util';
import {
mockPromise,
pressKey,
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index 13116fd..5bcef11 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -98,7 +98,7 @@
expression: string
): Promise<AutocompleteSuggestion[]> {
return this.restApiService
- .getSuggestedProjects(expression, MAX_AUTOCOMPLETE_RESULTS)
+ .getSuggestedRepos(expression, MAX_AUTOCOMPLETE_RESULTS)
.then(projects => {
if (!projects) {
return [];
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
index a0d49c8..7e3b896 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -94,7 +94,7 @@
});
test('Autocompletes projects', () => {
- stubRestApi('getSuggestedProjects').callsFake(() =>
+ stubRestApi('getSuggestedRepos').callsFake(() =>
Promise.resolve({Polygerrit: {id: 'test' as UrlEncodedRepoName}})
);
return element.fetchProjects('project', 'pol').then(s => {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 17d7516..f1e70b0 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -18,7 +18,6 @@
FilePathToDiffInfoMap,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
import {OpenFixPreviewEvent} from '../../../types/events';
import {getAppContext} from '../../../services/app-context';
@@ -34,6 +33,8 @@
import {resolve} from '../../../models/dependency';
import {createChangeUrl} from '../../../models/views/change';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {userModelToken} from '../../../models/user/user-model';
+import {modalStyles} from '../../../styles/gr-modal-styles';
interface FilePreview {
filepath: string;
@@ -42,8 +43,8 @@
@customElement('gr-apply-fix-dialog')
export class GrApplyFixDialog extends LitElement {
- @query('#applyFixOverlay')
- applyFixOverlay?: GrOverlay;
+ @query('#applyFixModal')
+ applyFixModal?: HTMLDialogElement;
@query('#applyFixDialog')
applyFixDialog?: GrDialog;
@@ -89,7 +90,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -97,7 +98,7 @@
super();
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
preferences => {
if (!preferences?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
@@ -106,7 +107,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
this.diffPrefs = diffPreferences;
@@ -116,6 +117,7 @@
static override styles = [
sharedStyles,
+ modalStyles,
css`
.diffContainer {
padding: var(--spacing-l) 0;
@@ -140,7 +142,7 @@
override render() {
return html`
- <gr-overlay id="applyFixOverlay" with-backdrop="">
+ <dialog id="applyFixModal" tabindex="-1">
<gr-dialog
id="applyFixDialog"
.confirmLabel=${this.isApplyFixLoading ? 'Saving...' : 'Apply Fix'}
@@ -151,43 +153,14 @@
>
${this.renderHeader()} ${this.renderMain()} ${this.renderFooter()}
</gr-dialog>
- </gr-overlay>
+ </dialog>
`;
}
- override updated() {
- this.updateDialogObserver();
- }
-
override disconnectedCallback() {
- this.removeDialogObserver();
super.disconnectedCallback();
}
- private removeDialogObserver() {
- this.dialogObserver?.disconnect();
- this.dialogObserver = undefined;
- this.observedDialog = undefined;
- }
-
- private updateDialogObserver() {
- if (
- this.applyFixDialog === this.observedDialog &&
- this.dialogObserver !== undefined
- ) {
- return;
- }
-
- this.removeDialogObserver();
- if (!this.applyFixDialog) return;
-
- this.observedDialog = this.applyFixDialog;
- this.dialogObserver = new ResizeObserver(() => {
- this.applyFixOverlay?.refit();
- });
- this.dialogObserver.observe(this.observedDialog);
- }
-
private renderHeader() {
return html`
<div slot="header">${this.currentFix?.description ?? ''}</div>
@@ -247,11 +220,8 @@
this.fixSuggestions = e.detail.fixSuggestions;
assert(this.fixSuggestions.length > 0, 'no fix in the event');
this.selectedFixIdx = 0;
- const promises = [];
- promises.push(
- this.showSelectedFixSuggestion(this.fixSuggestions[0]),
- this.applyFixOverlay?.open()
- );
+ this.applyFixModal?.showModal();
+ return this.showSelectedFixSuggestion(this.fixSuggestions[0]);
}
private async showSelectedFixSuggestion(fixSuggestion: FixSuggestionInfo) {
@@ -334,7 +304,7 @@
this.isApplyFixLoading = false;
fireCloseFixPreview(this, fixApplied);
- this.applyFixOverlay?.close();
+ this.applyFixModal?.close();
}
private computeTooltip() {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index 4d2d454..24dadf7 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -142,7 +142,7 @@
f2: diffInfo2,
})
);
- sinon.stub(element.applyFixOverlay!, 'open').returns(Promise.resolve());
+ sinon.stub(element.applyFixModal!, 'showModal');
});
test('dialog opens fetch and sets previews', async () => {
@@ -183,7 +183,7 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-overlay id="applyFixOverlay" tabindex="-1" with-backdrop="">
+ <dialog id="applyFixModal" tabindex="-1" open="">
<gr-dialog id="applyFixDialog" role="dialog">
<div slot="header">Fix fix_1</div>
<div slot="main"></div>
@@ -208,7 +208,7 @@
</gr-button>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`,
{ignoreAttributes: ['style']}
);
@@ -216,11 +216,12 @@
test('next button state updated when suggestions changed', async () => {
stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
- sinon.stub(element.applyFixOverlay!, 'open').returns(Promise.resolve());
await open(ONE_FIX);
await element.updateComplete;
assert.notOk(element.nextFix);
+ element.applyFixModal?.close();
+
await open(TWO_FIXES);
assert.ok(element.nextFix);
assert.notOk(element.nextFix!.disabled);
@@ -294,7 +295,7 @@
});
test('select fix forward and back of multiple suggested fixes', async () => {
- sinon.stub(element.applyFixOverlay!, 'open').returns(Promise.resolve());
+ sinon.stub(element.applyFixModal!, 'showModal');
await open(TWO_FIXES);
element.onNextFixClick(new CustomEvent('click'));
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 7570ac5..26043b32 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
@@ -234,8 +234,6 @@
*
* @param patchRange The patch-range object containing patchNum
* and basePatchNum properties to represent the range.
- * @param projectConfig Optional project config object to
- * include in the meta sub-object.
*/
getCommentsForPath(path: string, patchRange: PatchRange): CommentInfo[] {
let comments: CommentInfo[] = [];
@@ -373,8 +371,6 @@
*
* @param patchRange The patch-range object containing patchNum
* and basePatchNum properties to represent the range.
- * @param projectConfig Optional project config object to
- * include in the meta sub-object.
*/
getCommentsForFile(
file: PatchSetFile,
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 baf89c0..f0fbd64 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
@@ -67,7 +67,6 @@
waitForEventOnce,
fire,
} from '../../../utils/event-util';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {assertIsDefined} from '../../../utils/common-util';
import {DiffContextExpandedEventDetail} from '../../../embed/diff/gr-diff-builder/gr-diff-builder';
import {TokenHighlightLayer} from '../../../embed/diff/gr-diff-builder/token-highlight-layer';
@@ -84,7 +83,10 @@
import {deepEqual} from '../../../utils/deep-util';
import {Category} from '../../../api/checks';
import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
-import {CODE_MAX_LINES} from '../../../services/highlight/highlight-service';
+import {
+ CODE_MAX_LINES,
+ highlightServiceToken,
+} from '../../../services/highlight/highlight-service';
import {html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {ValueChangedEvent} from '../../../types/events';
@@ -95,6 +97,8 @@
} from '../../../utils/async-util';
import {subscribe} from '../../lit/subscription-controller';
import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {userModelToken} from '../../../models/user/user-model';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
const EMPTY_BLAME = 'No blame information for this diff.';
@@ -322,6 +326,8 @@
private readonly getChecksModel = resolve(this, checksModelToken);
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
// visible for testing
readonly reporting = getAppContext().reportingService;
@@ -330,10 +336,7 @@
private readonly restApiService = getAppContext().restApiService;
// visible for testing
- readonly userModel = getAppContext().userModel;
-
- // visible for testing
- readonly jsAPI = getAppContext().jsApiService;
+ readonly getUserModel = resolve(this, userModelToken);
// visible for testing
readonly syntaxLayer: GrSyntaxLayerWorker;
@@ -345,7 +348,10 @@
constructor() {
super();
- this.syntaxLayer = new GrSyntaxLayerWorker();
+ this.syntaxLayer = new GrSyntaxLayerWorker(
+ resolve(this, highlightServiceToken),
+ () => getAppContext().reportingService
+ );
this.renderPrefs = {
...this.renderPrefs,
use_lit_components: this.flags.isEnabled(
@@ -372,7 +378,7 @@
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => (this.loggedIn = loggedIn)
);
subscribe(
@@ -384,7 +390,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
this.prefs = diffPreferences;
}
@@ -548,7 +554,7 @@
async initLayers() {
const preferencesPromise = this.restApiService.getPreferences();
- await getPluginLoader().awaitPluginsLoaded();
+ await this.getPluginLoader().awaitPluginsLoaded();
const prefs = await preferencesPromise;
const enableTokenHighlight = !prefs?.disable_token_highlighting;
@@ -712,12 +718,13 @@
}
layers.push(this.syntaxLayer);
// Get layers from plugins (if any).
- layers.push(...this.jsAPI.getDiffLayers(path));
+ layers.push(...this.getPluginLoader().jsApiService.getDiffLayers(path));
return layers;
}
clear() {
- if (this.path) this.jsAPI.disposeDiffLayers(this.path);
+ if (this.path)
+ this.getPluginLoader().jsApiService.disposeDiffLayers(this.path);
this.layers = [];
}
@@ -847,8 +854,8 @@
const basePatchNum = toNumberOnly(this.patchRange.basePatchNum);
const patchNum = toNumberOnly(this.patchRange.patchNum);
- this.jsAPI
- .getCoverageAnnotationApis()
+ this.getPluginLoader()
+ .jsApiService.getCoverageAnnotationApis()
.then(coverageAnnotationApis => {
coverageAnnotationApis.forEach(coverageAnnotationApi => {
const provider = coverageAnnotationApi.getCoverageProvider();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index 598819b..14c2e00 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -53,11 +53,15 @@
import {GrAnnotationActionsInterface} from '../../shared/gr-js-api-interface/gr-annotation-actions-js-api';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken, UserModel} from '../../../models/user/user-model';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-diff-host tests', () => {
let element: GrDiffHost;
let account = createAccountDetailWithId(1);
let getDiffRestApiStub: SinonStub;
+ let userModel: UserModel;
setup(async () => {
stubRestApi('getAccount').callsFake(() => Promise.resolve(account));
@@ -70,6 +74,7 @@
// Fall back in case a test forgets to set one up
getDiffRestApiStub.returns(Promise.resolve(createDiff()));
await element.updateComplete;
+ userModel = testResolver(userModelToken);
});
suite('plugin layers', () => {
@@ -78,7 +83,7 @@
setup(async () => {
element = await fixture(html`<gr-diff-host></gr-diff-host>`);
getDiffLayersStub = sinon
- .stub(element.jsAPI, 'getDiffLayers')
+ .stub(testResolver(pluginLoaderToken).jsApiService, 'getDiffLayers')
.returns(pluginLayers);
element.changeNum = 123 as NumericChangeId;
element.change = createChange();
@@ -591,7 +596,7 @@
});
test('cannot create comments when not logged in', () => {
- element.userModel.setAccount(undefined);
+ userModel.setAccount(undefined);
element.patchRange = createPatchRange();
const showAuthRequireSpy = sinon.spy();
element.addEventListener('show-auth-required', showAuthRequireSpy);
@@ -1628,7 +1633,10 @@
})
);
getCoverageAnnotationApisStub = sinon
- .stub(element.jsAPI, 'getCoverageAnnotationApis')
+ .stub(
+ testResolver(pluginLoaderToken).jsApiService,
+ 'getCoverageAnnotationApis'
+ )
.returns(
Promise.resolve([
{
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
index 17004d9..cc1b1f2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
@@ -5,15 +5,14 @@
*/
import '../../shared/gr-button/gr-button';
import '../../shared/gr-diff-preferences/gr-diff-preferences';
-import '../../shared/gr-overlay/gr-overlay';
import {GrDiffPreferences} from '../../shared/gr-diff-preferences/gr-diff-preferences';
import {GrButton} from '../../shared/gr-button/gr-button';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {assertIsDefined} from '../../../utils/common-util';
import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css, PropertyValues} from 'lit';
+import {LitElement, html, css} from 'lit';
import {customElement, query, state} from 'lit/decorators.js';
import {ValueChangedEvent} from '../../../types/events';
+import {modalStyles} from '../../../styles/gr-modal-styles';
@customElement('gr-diff-preferences-dialog')
export class GrDiffPreferencesDialog extends LitElement {
@@ -23,13 +22,14 @@
@query('#cancelButton') private cancelButton?: GrButton;
- @query('#diffPrefsOverlay') private diffPrefsOverlay?: GrOverlay;
+ @query('#diffPrefsModal') private diffPrefsModal?: HTMLDialogElement;
@state() diffPrefsChanged?: boolean;
static override get styles() {
return [
sharedStyles,
+ modalStyles,
css`
.diffHeader,
.diffActions {
@@ -48,7 +48,7 @@
display: flex;
justify-content: flex-end;
}
- .diffPrefsOverlay gr-button {
+ .diffPrefsModal gr-button {
margin-left: var(--spacing-l);
}
div.edited:after {
@@ -65,7 +65,7 @@
override render() {
return html`
- <gr-overlay id="diffPrefsOverlay" with-backdrop="">
+ <dialog id="diffPrefsModal" tabindex="-1">
<div role="dialog" aria-labelledby="diffPreferencesTitle">
<h3
class="heading-3 diffHeader ${this.diffPrefsChanged
@@ -100,16 +100,10 @@
</gr-button>
</div>
</div>
- </gr-overlay>
+ </dialog>
`;
}
- override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('diffPrefsChanged')) {
- this.onDiffPrefsChanged();
- }
- }
-
getFocusStops() {
assertIsDefined(this.diffPreferences, 'diffPreferences');
assertIsDefined(this.saveButton, 'saveButton');
@@ -128,27 +122,18 @@
private readonly handleCancelDiff = (e: MouseEvent) => {
e.stopPropagation();
- assertIsDefined(this.diffPrefsOverlay, 'diffPrefsOverlay');
- this.diffPrefsOverlay.close();
+ assertIsDefined(this.diffPrefsModal, 'diffPrefsModal');
+ this.diffPrefsModal.close();
};
- private onDiffPrefsChanged() {
- assertIsDefined(this.diffPrefsOverlay, 'diffPrefsOverlay');
- this.diffPrefsOverlay.setFocusStops(this.getFocusStops());
- }
-
open() {
- assertIsDefined(this.diffPrefsOverlay, 'diffPrefsOverlay');
- this.diffPrefsOverlay.open().then(() => {
- const focusStops = this.getFocusStops();
- this.diffPrefsOverlay!.setFocusStops(focusStops);
- this.resetFocus();
- });
+ assertIsDefined(this.diffPrefsModal, 'diffPrefsModal');
+ this.diffPrefsModal.showModal();
}
private async handleSaveDiffPreferences() {
assertIsDefined(this.diffPreferences, 'diffPreferences');
- assertIsDefined(this.diffPrefsOverlay, 'diffPrefsOverlay');
+ assertIsDefined(this.diffPrefsModal, 'diffPrefsModal');
await this.diffPreferences.save();
this.dispatchEvent(
new CustomEvent('reload-diff-preference', {
@@ -156,7 +141,7 @@
bubbles: false,
})
);
- this.diffPrefsOverlay.close();
+ this.diffPrefsModal.close();
}
private readonly handleHasUnsavedChangesChanged = (
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
index 1b484b0..7fc1044 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
@@ -36,13 +36,7 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-overlay
- aria-hidden="true"
- id="diffPrefsOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="diffPrefsModal" tabindex="-1">
<div aria-labelledby="diffPreferencesTitle" role="dialog">
<h3 class="diffHeader heading-3" id="diffPreferencesTitle">
Diff Preferences
@@ -71,7 +65,7 @@
</gr-button>
</div>
</div>
- </gr-overlay>
+ </dialog>
`
);
});
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 6ad8e2f..7d3fe6f 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
@@ -85,9 +85,12 @@
ValueChangedEvent,
} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
-import {GerritView} from '../../../services/router/router-model';
-import {assertIsDefined} from '../../../utils/common-util';
-import {Key, toggleClass} from '../../../utils/dom-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 {filter, take, switchMap} from 'rxjs/operators';
@@ -100,7 +103,6 @@
import {LoadingStatus} from '../../../models/change/change-model';
import {DisplayLine} from '../../../api/diff';
import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
-import {browserModelToken} from '../../../models/browser/browser-model';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
import {resolve} from '../../../models/dependency';
@@ -122,6 +124,9 @@
import {createChangeUrl} from '../../../models/views/change';
import {createEditUrl} from '../../../models/views/edit';
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';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -158,8 +163,8 @@
@query('#reviewed')
reviewed?: HTMLInputElement;
- @query('#downloadOverlay')
- downloadOverlay?: GrOverlay;
+ @query('#downloadModal')
+ downloadModal?: HTMLDialogElement;
@query('#downloadDialog')
downloadDialog?: GrDownloadDialog;
@@ -288,20 +293,13 @@
private readonly restApiService = getAppContext().restApiService;
- // Private but used in tests.
- readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
- // Private but used in tests.
- readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- // Private but used in tests.
- readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getBrowserModel = resolve(this, browserModelToken);
-
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
@@ -396,7 +394,7 @@
private setupSubscriptions() {
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => {
this.loggedIn = loggedIn;
}
@@ -417,14 +415,14 @@
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
preferences => {
this.userPrefs = preferences;
}
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
this.prefs = diffPreferences;
}
@@ -478,8 +476,8 @@
switchMap(() =>
combineLatest([
this.getChangeModel().patchNum$,
- this.routerModel.routerView$,
- this.userModel.diffPreferences$,
+ this.getRouterModel().routerView$,
+ this.getUserModel().diffPreferences$,
this.getChangeModel().reviewedFiles$,
]).pipe(
filter(
@@ -508,6 +506,7 @@
return [
a11yStyles,
sharedStyles,
+ modalStyles,
css`
:host {
display: block;
@@ -1019,7 +1018,7 @@
@reload-diff-preference=${this.handleReloadingDiffPreference}
>
</gr-diff-preferences-dialog>
- <gr-overlay id="downloadOverlay">
+ <dialog id="downloadModal" tabindex="-1">
<gr-download-dialog
id="downloadDialog"
.change=${this.change}
@@ -1027,7 +1026,7 @@
.config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
- </gr-overlay>`;
+ </dialog>`;
}
/**
@@ -1287,18 +1286,31 @@
}
private handleOpenDownloadDialog() {
- assertIsDefined(this.downloadOverlay, 'downloadOverlay');
- this.downloadOverlay.open().then(() => {
- assertIsDefined(this.downloadOverlay, 'downloadOverlay');
- assertIsDefined(this.downloadDialog, 'downloadOverlay');
- this.downloadOverlay.setFocusStops(this.downloadDialog.getFocusStops());
+ assertIsDefined(this.downloadModal, 'downloadModal');
+ this.downloadModal.showModal();
+ whenVisible(this.downloadModal, () => {
+ assertIsDefined(this.downloadModal, 'downloadModal');
+ assertIsDefined(this.downloadDialog, 'downloadDialog');
this.downloadDialog.focus();
+ const downloadCommands = queryAndAssert(
+ this.downloadDialog,
+ 'gr-download-commands'
+ );
+ const paperTabs = queryAndAssert<PaperTabsElement>(
+ downloadCommands,
+ 'paper-tabs'
+ );
+ // Paper Tabs normally listen to 'iron-resize' event to call this method.
+ // After migrating to Dialog element, this event is no longer fired
+ // which means this method is not called which ends up styling the
+ // selected paper tab with an underline.
+ paperTabs._onTabSizingChanged();
});
}
private handleDownloadDialogClose() {
- assertIsDefined(this.downloadOverlay, 'downloadOverlay');
- this.downloadOverlay.close();
+ assertIsDefined(this.downloadModal, 'downloadModal');
+ this.downloadModal.close();
}
private handleUpToChange() {
@@ -1315,9 +1327,9 @@
handleToggleDiffMode() {
if (!this.userPrefs) return;
if (this.userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
- this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+ this.getUserModel().updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
}
@@ -1405,7 +1417,7 @@
const cursorAddress = this.cursor?.getAddress();
const editUrl = createEditUrl({
changeNum: this.change._number,
- project: this.change.project,
+ repo: this.change.project,
path: this.path,
patchNum: this.patchRange.patchNum,
lineNum: cursorAddress?.number,
@@ -1523,7 +1535,7 @@
if (!this.path) return;
const url = createDiffUrl({
changeNum: this.changeNum,
- project: this.change.project,
+ repo: this.change.project,
path: this.path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
@@ -1653,10 +1665,10 @@
this.commitRange = undefined;
this.focusLineNum = undefined;
- if (viewState.changeNum && viewState.project) {
+ if (viewState.changeNum && viewState.repo) {
this.restApiService.setInProjectLookup(
viewState.changeNum,
- viewState.project
+ viewState.repo
);
}
@@ -1786,7 +1798,7 @@
if (!change || !patchRange || !path) return '';
return createDiffUrl({
changeNum: change._number,
- project: change.project,
+ repo: change.project,
path,
patchNum: patchRange.patchNum,
basePatchNum: patchRange.basePatchNum,
@@ -1994,7 +2006,7 @@
// Private but used in tests.
computeDownloadFileLink(
- project: RepoName,
+ repo: RepoName,
changeNum: NumericChangeId,
patchRange: PatchRange,
path: string,
@@ -2013,7 +2025,7 @@
}
}
let url =
- changeBaseURL(project, changeNum, patchNum) +
+ changeBaseURL(repo, changeNum, patchNum) +
`/files/${encodeURIComponent(path)}/download`;
if (parent) url += `?parent=${parent}`;
@@ -2022,12 +2034,12 @@
// Private but used in tests.
computeDownloadPatchLink(
- project: RepoName,
+ repo: RepoName,
changeNum: NumericChangeId,
patchRange: PatchRange,
path: string
) {
- let url = changeBaseURL(project, changeNum, patchRange.patchNum);
+ let url = changeBaseURL(repo, changeNum, patchRange.patchNum);
url += '/patch?zip&path=' + encodeURIComponent(path);
return url;
}
@@ -2266,7 +2278,7 @@
}
private handleReloadingDiffPreference() {
- this.userModel.getDiffPreferences();
+ this.getUserModel().getDiffPreferences();
}
private computeCanEdit() {
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 b6e26ab..6a565eb 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
@@ -20,12 +20,14 @@
queryAndAssert,
stubReporting,
stubRestApi,
- stubUsers,
waitEventLoop,
waitUntil,
} from '../../../test/test-utils';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {
createRevisions,
createComment as createCommentGeneric,
@@ -60,7 +62,11 @@
import {Files, GrDiffView} from './gr-diff-view';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon';
-import {LoadingStatus} from '../../../models/change/change-model';
+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';
@@ -70,6 +76,15 @@
import {Key} from '../../../utils/dom-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+ commentsModelToken,
+ CommentsModel,
+} from '../../../models/comments/comments-model';
+import {
+ BrowserModel,
+ browserModelToken,
+} from '../../../models/browser/browser-model';
function createComment(
id: string,
@@ -93,6 +108,10 @@
let diffCommentsStub;
let getDiffRestApiStub: SinonStub;
let setUrlStub: SinonStub;
+ let changeModel: ChangeModel;
+ let commentsModel: CommentsModel;
+ let browserModel: BrowserModel;
+ let userModel: UserModel;
function getFilesFromFileList(fileList: string[]): Files {
const changeFilesByPath = fileList.reduce((files, path) => {
@@ -140,8 +159,12 @@
],
});
await element.updateComplete;
+ commentsModel = testResolver(commentsModelToken);
+ changeModel = testResolver(changeModelToken);
+ browserModel = testResolver(browserModelToken);
+ userModel = testResolver(userModelToken);
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {},
@@ -192,7 +215,7 @@
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -202,7 +225,7 @@
});
test('comment url resolves to comment.patch_set vs latest', () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -265,7 +288,7 @@
});
test('unchanged diff X vs latest from comment links navigates to base vs X', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -284,7 +307,7 @@
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'isFileUnchanged').returns(true);
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -311,7 +334,7 @@
});
test('unchanged diff Base vs latest from comment does not navigate', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -330,7 +353,7 @@
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'isFileUnchanged').returns(true);
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -385,7 +408,7 @@
});
test('diff toast to go to latest is shown and not base', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -405,7 +428,7 @@
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.change = undefined;
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -421,7 +444,7 @@
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
- project: 'p' as RepoName,
+ repo: 'p' as RepoName,
commentId: 'c1' as UrlEncodedCommentId,
commentLink: true,
};
@@ -439,7 +462,7 @@
test('renders', async () => {
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
- element.getBrowserModel().setScreenWidth(0);
+ browserModel.setScreenWidth(0);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
@@ -608,14 +631,10 @@
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
<gr-diff-preferences-dialog id="diffPreferencesDialog">
</gr-diff-preferences-dialog>
- <gr-overlay
- aria-hidden="true"
- id="downloadOverlay"
- style="outline: none; display: none;"
- >
+ <dialog id="downloadModal" tabindex="-1">
<gr-download-dialog id="downloadDialog" role="dialog">
</gr-download-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -623,7 +642,7 @@
test('keyboard shortcuts', async () => {
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
- element.getBrowserModel().setScreenWidth(0);
+ browserModel.setScreenWidth(0);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
@@ -1054,12 +1073,10 @@
assert.equal(setUrlStub.callCount, 5);
assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
- assertIsDefined(element.downloadOverlay);
- const downloadOverlayStub = sinon
- .stub(element.downloadOverlay, 'open')
- .returns(Promise.resolve());
+ assertIsDefined(element.downloadModal);
+ const downloadModalStub = sinon.stub(element.downloadModal, 'showModal');
pressKey(element, 'd');
- assert.isTrue(downloadOverlayStub.called);
+ assert.isTrue(downloadModalStub.called);
});
test('keyboard shortcuts with old patch number', () => {
@@ -1529,7 +1546,7 @@
'automatically called',
async () => {
const setReviewedFileStatusStub = sinon
- .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+ .stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus');
@@ -1541,15 +1558,15 @@
...createDefaultDiffPrefs(),
manual_review: true,
};
- element.userModel.setDiffPreferences(diffPreferences);
- element.getChangeModel().setState({
+ userModel.setDiffPreferences(diffPreferences);
+ changeModel.setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- element.routerModel.setState({
+ testResolver(routerModelToken).setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 2 as RevisionPatchSetNum,
@@ -1564,7 +1581,7 @@
assert.isFalse(setReviewedFileStatusStub.called);
// if prefs are updated then the reviewed status should not be set again
- element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+ userModel.setDiffPreferences(createDefaultDiffPrefs());
await element.updateComplete;
assert.isFalse(setReviewedFileStatusStub.called);
@@ -1573,7 +1590,7 @@
test('_prefs.manual_review false means set reviewed is called', async () => {
const setReviewedFileStatusStub = sinon
- .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+ .stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
assertIsDefined(element.diffHost);
@@ -1583,15 +1600,15 @@
...createDefaultDiffPrefs(),
manual_review: false,
};
- element.userModel.setDiffPreferences(diffPreferences);
- element.getChangeModel().setState({
+ userModel.setDiffPreferences(diffPreferences);
+ changeModel.setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- element.routerModel.setState({
+ testResolver(routerModelToken).setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 22 as RevisionPatchSetNum,
@@ -1607,7 +1624,7 @@
});
test('file review status', async () => {
- element.getChangeModel().setState({
+ changeModel.setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
@@ -1615,14 +1632,14 @@
});
element.loggedIn = true;
const saveReviewedStub = sinon
- .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+ .stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
- element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+ userModel.setDiffPreferences(createDefaultDiffPrefs());
- element.routerModel.setState({
+ testResolver(routerModelToken).setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 2 as RevisionPatchSetNum,
@@ -1635,7 +1652,7 @@
await waitUntil(() => saveReviewedStub.called);
- element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', true);
+ changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
await element.updateComplete;
const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
@@ -1660,7 +1677,7 @@
false,
]);
- element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', false);
+ changeModel.updateStateFileReviewed('/COMMIT_MSG', false);
await element.updateComplete;
reviewedStatusCheckBox.click();
@@ -1677,7 +1694,7 @@
element.viewState = {
view: GerritView.DIFF,
changeNum: 42 as NumericChangeId,
- project: 'test' as RepoName,
+ repo: 'test' as RepoName,
};
await element.updateComplete;
@@ -1688,7 +1705,7 @@
test('file review status with edit loaded', async () => {
const saveReviewedStub = sinon.stub(
- element.getChangeModel(),
+ changeModel,
'setReviewedFilesStatus'
);
@@ -1730,9 +1747,9 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
- element.getBrowserModel().setScreenWidth(0);
+ browserModel.setScreenWidth(0);
- const userStub = stubUsers('updatePreferences');
+ const userStub = sinon.stub(userModel, 'updatePreferences');
await element.updateComplete;
// The mode selected in the view state reflects the selected option.
@@ -1926,7 +1943,7 @@
});
test('handleToggleDiffMode', () => {
- const userStub = stubUsers('updatePreferences');
+ const userStub = sinon.stub(userModel, 'updatePreferences');
element.userPrefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
@@ -2367,7 +2384,7 @@
view: GerritView.DIFF,
patchNum: 1 as RevisionPatchSetNum,
changeNum: 101 as NumericChangeId,
- project: 'test-project' as RepoName,
+ repo: 'test-project' as RepoName,
path: 'file1',
};
element.patchRange = {
@@ -2392,7 +2409,7 @@
view: GerritView.DIFF,
patchNum: 1 as RevisionPatchSetNum,
changeNum: 101 as NumericChangeId,
- project: 'test-project' as RepoName,
+ repo: 'test-project' as RepoName,
path: 'file2',
};
element.patchRange = {
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 a273a3e..6cfefe5 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
@@ -8,7 +8,6 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-dropdown/gr-dropdown';
-import '../../shared/gr-overlay/gr-overlay';
import {GrEditAction, GrEditConstants} from '../gr-edit-constants';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {ChangeInfo, RevisionPatchSetNum} from '../../../types/common';
@@ -29,17 +28,18 @@
import {LitElement, html, css} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {IronInputElement} from '@polymer/iron-input/iron-input';
import {createEditUrl} from '../../../models/views/edit';
import {resolve} from '../../../models/dependency';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {whenVisible} from '../../../utils/dom-util';
@customElement('gr-edit-controls')
export class GrEditControls extends LitElement {
// private but used in test
@query('#newPathIronInput') newPathIronInput?: IronInputElement;
- @query('#overlay') protected overlay?: GrOverlay;
+ @query('#modal') modal?: HTMLDialogElement;
// private but used in test
@query('#openDialog') openDialog?: GrDialog;
@@ -81,6 +81,7 @@
static override get styles() {
return [
sharedStyles,
+ modalStyles,
css`
:host {
align-items: center;
@@ -137,10 +138,10 @@
override render() {
return html`
${this.actions.map(action => this.renderAction(action))}
- <gr-overlay id="overlay" with-backdrop="">
+ <dialog id="modal" tabindex="-1">
${this.renderOpenDialog()} ${this.renderDeleteDialog()}
${this.renderRenameDialog()} ${this.renderRestoreDialog()}
- </gr-overlay>
+ </dialog>
`;
}
@@ -309,7 +310,7 @@
this.path = path;
}
assertIsDefined(this.openDialog, 'openDialog');
- return this.showDialog(this.openDialog);
+ this.showDialog(this.openDialog);
}
openDeleteDialog(path?: string) {
@@ -317,7 +318,7 @@
this.path = path;
}
assertIsDefined(this.deleteDialog, 'deleteDialog');
- return this.showDialog(this.deleteDialog);
+ this.showDialog(this.deleteDialog);
}
openRenameDialog(path?: string) {
@@ -325,7 +326,7 @@
this.path = path;
}
assertIsDefined(this.renameDialog, 'renameDialog');
- return this.showDialog(this.renameDialog);
+ this.showDialog(this.renameDialog);
}
openRestoreDialog(path?: string) {
@@ -333,7 +334,7 @@
if (path) {
this.path = path;
}
- return this.showDialog(this.restoreDialog);
+ this.showDialog(this.restoreDialog);
}
/**
@@ -361,23 +362,20 @@
// private but used in test
showDialog(dialog: GrDialog) {
- assertIsDefined(this.overlay, 'overlay');
+ assertIsDefined(this.modal, 'modal');
// Some dialogs may not fire their on-close event when closed in certain
// ways (e.g. by clicking outside the dialog body). This call prevents
- // multiple dialogs from being shown in the same overlay.
+ // multiple dialogs from being shown in the same modal.
this.hideAllDialogs();
- return this.overlay.open().then(() => {
+ this.modal.showModal();
+ whenVisible(this.modal, () => {
dialog.classList.toggle('invisible', false);
const autocomplete = queryUtil<GrAutocomplete>(dialog, 'gr-autocomplete');
if (autocomplete) {
autocomplete.focus();
}
- setTimeout(() => {
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.center();
- }, 1);
});
}
@@ -412,8 +410,8 @@
dialog.classList.toggle('invisible', true);
- assertIsDefined(this.overlay, 'overlay');
- this.overlay.close();
+ assertIsDefined(this.modal, 'modal');
+ this.modal.close();
}
private readonly handleDialogCancel = (e: Event) => {
@@ -429,7 +427,7 @@
assertIsDefined(this.patchNum, 'patchset number');
const url = createEditUrl({
changeNum: this.change._number,
- project: this.change.project,
+ repo: this.change.project,
path: this.path,
patchNum: this.patchNum,
});
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index b4469db..31283ad 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -7,7 +7,12 @@
import './gr-edit-controls';
import {GrEditControls} from './gr-edit-controls';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {queryAll, stubRestApi, waitUntil} from '../../../test/test-utils';
+import {
+ queryAll,
+ stubRestApi,
+ waitUntil,
+ waitUntilVisible,
+} from '../../../test/test-utils';
import {createChange, createRevision} from '../../../test/test-data-generators';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {
@@ -86,13 +91,7 @@
>
Restore
</gr-button>
- <gr-overlay
- aria-hidden="true"
- id="overlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="modal" tabindex="-1">
<gr-dialog
class="dialog invisible"
confirm-label="Confirm"
@@ -180,7 +179,7 @@
</iron-input>
</div>
</gr-dialog>
- </gr-overlay>
+ </dialog>
`
);
});
@@ -218,7 +217,7 @@
assert.isFalse(hideDialogStub.called);
queryAndAssert<GrButton>(element, '#open').click();
element.patchNum = 1 as RevisionPatchSetNum;
- await showDialogSpy.lastCall.returnValue;
+ await waitUntilVisible(element.modal!);
assert.isTrue(hideDialogStub.called);
assert.isTrue(element.openDialog!.disabled);
assert.isFalse(queryStub.called);
@@ -241,17 +240,16 @@
test('cancel', async () => {
queryAndAssert<GrButton>(element, '#open').click();
- return showDialogSpy.lastCall.returnValue.then(async () => {
- assert.isTrue(element.openDialog!.disabled);
- openAutoComplete.noDebounce = true;
- openAutoComplete.text = 'src/test.cpp';
- await element.updateComplete;
- await waitUntil(() => !element.openDialog!.disabled);
- queryAndAssert<GrButton>(element.openDialog, 'gr-button').click();
- assert.isFalse(setUrlStub.called);
- await waitUntil(() => closeDialogSpy.called);
- assert.equal(element.path, '');
- });
+ await waitUntilVisible(element.modal!);
+ assert.isTrue(element.openDialog!.disabled);
+ openAutoComplete.noDebounce = true;
+ openAutoComplete.text = 'src/test.cpp';
+ await element.updateComplete;
+ await waitUntil(() => !element.openDialog!.disabled);
+ queryAndAssert<GrButton>(element.openDialog, 'gr-button').click();
+ assert.isFalse(setUrlStub.called);
+ await waitUntil(() => closeDialogSpy.called);
+ assert.equal(element.path, '');
});
});
@@ -324,21 +322,20 @@
assert.isFalse(closeDialogSpy.called);
});
- test('cancel', () => {
+ test('cancel', async () => {
queryAndAssert<GrButton>(element, '#delete').click();
- return showDialogSpy.lastCall.returnValue.then(async () => {
- assert.isTrue(element.deleteDialog!.disabled);
- queryAndAssert<GrAutocomplete>(
- element.deleteDialog,
- 'gr-autocomplete'
- ).text = 'src/test.cpp';
- await element.updateComplete;
- await waitUntil(() => !element.deleteDialog!.disabled);
- queryAndAssert<GrButton>(element.deleteDialog, 'gr-button').click();
- assert.isFalse(eventStub.called);
- assert.isTrue(closeDialogSpy.called);
- await waitUntil(() => element.path === '');
- });
+ await waitUntilVisible(element.modal!);
+ assert.isTrue(element.deleteDialog!.disabled);
+ queryAndAssert<GrAutocomplete>(
+ element.deleteDialog,
+ 'gr-autocomplete'
+ ).text = 'src/test.cpp';
+ await element.updateComplete;
+ await waitUntil(() => !element.deleteDialog!.disabled);
+ queryAndAssert<GrButton>(element.deleteDialog, 'gr-button').click();
+ assert.isFalse(eventStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ await waitUntil(() => element.path === '');
});
});
@@ -421,22 +418,21 @@
assert.isFalse(closeDialogSpy.called);
});
- test('cancel', () => {
+ test('cancel', async () => {
queryAndAssert<GrButton>(element, '#rename').click();
- return showDialogSpy.lastCall.returnValue.then(async () => {
- assert.isTrue(element.renameDialog!.disabled);
- queryAndAssert<GrAutocomplete>(
- element.renameDialog,
- 'gr-autocomplete'
- ).text = 'src/test.cpp';
- element.newPathIronInput!.bindValue = 'src/test.newPath';
- await element.updateComplete;
- assert.isFalse(element.renameDialog!.disabled);
- queryAndAssert<GrButton>(element.renameDialog, 'gr-button').click();
- assert.isFalse(eventStub.called);
- assert.isTrue(closeDialogSpy.called);
- await waitUntil(() => element.path === '');
- });
+ await waitUntilVisible(element.modal!);
+ assert.isTrue(element.renameDialog!.disabled);
+ queryAndAssert<GrAutocomplete>(
+ element.renameDialog,
+ 'gr-autocomplete'
+ ).text = 'src/test.cpp';
+ element.newPathIronInput!.bindValue = 'src/test.newPath';
+ await element.updateComplete;
+ assert.isFalse(element.renameDialog!.disabled);
+ queryAndAssert<GrButton>(element.renameDialog, 'gr-button').click();
+ assert.isFalse(eventStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ await waitUntil(() => element.path === '');
});
});
@@ -455,56 +451,53 @@
);
});
- test('restore', () => {
+ test('restore', async () => {
restoreStub.returns(Promise.resolve({ok: true}));
element.path = 'src/test.cpp';
queryAndAssert<GrButton>(element, '#restore').click();
- return showDialogSpy.lastCall.returnValue.then(async () => {
- queryAndAssert<GrButton>(
- element.restoreDialog,
- 'gr-button[primary]'
- ).click();
- await element.updateComplete;
+ await waitUntilVisible(element.modal!);
+ queryAndAssert<GrButton>(
+ element.restoreDialog,
+ 'gr-button[primary]'
+ ).click();
+ await element.updateComplete;
- assert.isTrue(restoreStub.called);
- assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
- return restoreStub.lastCall.returnValue.then(() => {
- assert.equal(element.path, '');
- assert.equal(eventStub.firstCall.args[0].type, 'reload');
- assert.isTrue(closeDialogSpy.called);
- });
+ assert.isTrue(restoreStub.called);
+ assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
+ return restoreStub.lastCall.returnValue.then(() => {
+ assert.equal(element.path, '');
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
+ assert.isTrue(closeDialogSpy.called);
});
});
- test('restore fails', () => {
+ test('restore fails', async () => {
restoreStub.returns(Promise.resolve({ok: false}));
element.path = 'src/test.cpp';
queryAndAssert<GrButton>(element, '#restore').click();
- return showDialogSpy.lastCall.returnValue.then(async () => {
- queryAndAssert<GrButton>(
- element.restoreDialog,
- 'gr-button[primary]'
- ).click();
- await element.updateComplete;
+ await waitUntilVisible(element.modal!);
+ queryAndAssert<GrButton>(
+ element.restoreDialog,
+ 'gr-button[primary]'
+ ).click();
+ await element.updateComplete;
- assert.isTrue(restoreStub.called);
- assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
- return restoreStub.lastCall.returnValue.then(() => {
- assert.isFalse(eventStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
+ assert.isTrue(restoreStub.called);
+ assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
+ return restoreStub.lastCall.returnValue.then(() => {
+ assert.isFalse(eventStub.called);
+ assert.isFalse(closeDialogSpy.called);
});
});
- test('cancel', () => {
+ test('cancel', async () => {
element.path = 'src/test.cpp';
queryAndAssert<GrButton>(element, '#restore').click();
- return showDialogSpy.lastCall.returnValue.then(() => {
- queryAndAssert<GrButton>(element.restoreDialog, 'gr-button').click();
- assert.isFalse(eventStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element.path, '');
- });
+ await waitUntilVisible(element.modal!);
+ queryAndAssert<GrButton>(element.restoreDialog, 'gr-button').click();
+ assert.isFalse(eventStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element.path, '');
});
});
@@ -546,12 +539,12 @@
});
test('openOpenDialog', async () => {
- await element.openOpenDialog('test/path.cpp');
+ element.openOpenDialog('test/path.cpp');
assert.isFalse(element.openDialog!.hasAttribute('hidden'));
- assert.equal(
- queryAndAssert<GrAutocomplete>(element.openDialog, 'gr-autocomplete')
- .text,
- 'test/path.cpp'
+ await waitUntil(
+ () =>
+ queryAndAssert<GrAutocomplete>(element.openDialog, 'gr-autocomplete')
+ .text === 'test/path.cpp'
);
});
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 a15a575..8e346c3 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
@@ -33,6 +33,8 @@
import {ShortcutController} from '../../lit/shortcut-controller';
import {editViewModelToken, EditViewState} from '../../../models/views/edit';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
@@ -85,11 +87,11 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly storage = getAppContext().storageService;
-
private readonly reporting = getAppContext().reportingService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getStorage = resolve(this, storageServiceToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
@@ -109,7 +111,7 @@
});
subscribe(
this,
- () => this.userModel.editPreferences$,
+ () => this.getUserModel().editPreferences$,
editPreferences => (this.editPrefs = editPreferences)
);
subscribe(
@@ -379,7 +381,9 @@
assertIsDefined(patchNum, 'patchset number');
assertIsDefined(path, 'path');
- const storedContent = this.storage.getEditableContentItem(this.storageKey);
+ const storedContent = this.getStorage().getEditableContentItem(
+ this.storageKey
+ );
return this.restApiService
.getFileContent(changeNum, path, patchNum)
@@ -418,7 +422,7 @@
this.saving = true;
this.showAlert(SAVING_MESSAGE);
- this.storage.eraseEditableContentItem(this.storageKey);
+ this.getStorage().eraseEditableContentItem(this.storageKey);
if (!this.newContent)
return Promise.reject(new Error('new content undefined'));
return this.restApiService
@@ -499,9 +503,9 @@
const content = e.detail.value;
if (content) {
this.newContent = e.detail.value;
- this.storage.setEditableContentItem(this.storageKey, content);
+ this.getStorage().setEditableContentItem(this.storageKey, content);
} else {
- this.storage.eraseEditableContentItem(this.storageKey);
+ this.getStorage().eraseEditableContentItem(this.storageKey);
}
},
STORAGE_DEBOUNCE_INTERVAL_MS
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 52581ed..d428e18 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
@@ -13,7 +13,6 @@
pressKey,
query,
stubRestApi,
- stubStorage,
} from '../../../test/test-utils';
import {
EDIT,
@@ -32,6 +31,8 @@
import {EventType} from '../../../types/events';
import {Modifier} from '../../../utils/dom-util';
import {testResolver} from '../../../test/common-test-setup';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {StorageService} from '../../../services/storage/gr-storage';
suite('gr-editor-view tests', () => {
let element: GrEditorView;
@@ -40,6 +41,7 @@
let saveFileStub: sinon.SinonStub;
let changeDetailStub: sinon.SinonStub;
let navigateStub: sinon.SinonStub;
+ let storageService: StorageService;
setup(async () => {
element = await fixture(html`<gr-editor-view></gr-editor-view>`);
@@ -53,6 +55,7 @@
};
element.latestPatchsetNumber = 1 as PatchSetNumber;
await element.updateComplete;
+ storageService = testResolver(storageServiceToken);
});
test('render', () => {
@@ -178,7 +181,7 @@
});
test('reacts to content-change event', async () => {
- const storageStub = stubStorage('setEditableContentItem');
+ const storageStub = sinon.stub(storageService, 'setEditableContentItem');
element.newContent = 'test';
await element.updateComplete;
query<GrEndpointDecorator>(element, '#editorEndpoint')!.dispatchEvent(
@@ -219,7 +222,7 @@
test('file modification and save, !ok response', async () => {
const saveSpy = sinon.spy(element, 'saveEdit');
- const eraseStub = stubStorage('eraseEditableContentItem');
+ const eraseStub = sinon.stub(storageService, 'eraseEditableContentItem');
const alertStub = sinon.stub(element, 'showAlert');
saveFileStub.returns(Promise.resolve({ok: false}));
element.newContent = newText;
@@ -355,7 +358,7 @@
element.newContent = 'initial';
element.content = 'initial';
element.type = 'initial';
- stubStorage('getEditableContentItem').returns(null);
+ sinon.stub(storageService, 'getEditableContentItem').returns(null);
});
test('res.ok', () => {
@@ -512,7 +515,7 @@
suite('gr-storage caching', () => {
test('local edit exists', () => {
- stubStorage('getEditableContentItem').returns({
+ sinon.stub(storageService, 'getEditableContentItem').returns({
message: 'pending edit',
updated: 0,
});
@@ -544,7 +547,7 @@
});
test('local edit exists, is same as remote edit', () => {
- stubStorage('getEditableContentItem').returns({
+ sinon.stub(storageService, 'getEditableContentItem').returns({
message: 'pending edit',
updated: 0,
});
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index c05e4c4..7cadddc 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -53,7 +53,7 @@
RpcLogEvent,
TitleChangeEventDetail,
} from '../types/events';
-import {GerritView} from '../services/router/router-model';
+import {GerritView, routerModelToken} from '../services/router/router-model';
import {Execution, LifeCycle} from '../constants/reporting';
import {fireIronAnnounce} from '../utils/event-util';
import {resolve} from '../models/dependency';
@@ -73,6 +73,7 @@
import {createSearchUrl, SearchViewState} from '../models/views/search';
import {createSettingsUrl} from '../models/views/settings';
import {createDashboardUrl} from '../models/views/dashboard';
+import {userModelToken} from '../models/user/user-model';
interface ErrorInfo {
text: string;
@@ -80,6 +81,12 @@
moreInfo?: string;
}
+/**
+ * This is simple hacky way for allowing certain plugin screens to hide the
+ * header and the footer of the Gerrit page.
+ */
+const WHITE_LISTED_FULL_SCREEN_PLUGINS = ['git_source_editor/screen/edit'];
+
// TODO(TS): implement AppElement interface from gr-app-types.ts
@customElement('gr-app-element')
export class GrAppElement extends LitElement {
@@ -161,9 +168,9 @@
private readonly shortcuts = new ShortcutController(this);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- private readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
constructor() {
super();
@@ -210,7 +217,7 @@
subscribe(
this,
- () => this.userModel.preferenceTheme$,
+ () => this.getUserModel().preferenceTheme$,
theme => {
this.theme = theme;
this.applyTheme();
@@ -218,7 +225,7 @@
);
subscribe(
this,
- () => this.routerModel.routerView$,
+ () => this.getRouterModel().routerView$,
view => {
this.view = view;
if (view) this.errorView?.classList.remove('show');
@@ -257,14 +264,14 @@
// TODO(milutin): Remove saving preferences after while. This code is
// for migration.
if (window.localStorage.getItem('dark-theme')) {
- this.userModel.updatePreferences({theme: AppTheme.DARK});
+ this.getUserModel().updatePreferences({theme: AppTheme.DARK});
window.localStorage.removeItem('dark-theme');
this.reporting.reportExecution(
Execution.REACHABLE_CODE,
'Dark theme was migrated from localstorage'
);
} else if (window.localStorage.getItem('light-theme')) {
- this.userModel.updatePreferences({theme: AppTheme.LIGHT});
+ this.getUserModel().updatePreferences({theme: AppTheme.LIGHT});
window.localStorage.removeItem('light-theme');
this.reporting.reportExecution(
Execution.REACHABLE_CODE,
@@ -363,16 +370,7 @@
return html`
<gr-css-mixins></gr-css-mixins>
<gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
- <gr-main-header
- id="mainHeader"
- .searchQuery=${(this.params as SearchViewState)?.query}
- @mobile-search=${this.mobileSearchToggle}
- @show-keyboard-shortcuts=${this.showKeyboardShortcuts}
- .mobileSearchHidden=${!this.mobileSearch}
- .loginUrl=${this.loginUrl}
- ?aria-hidden=${this.footerHeaderAriaHidden}
- >
- </gr-main-header>
+ ${this.renderHeader()}
<main ?aria-hidden=${this.mainAriaHidden}>
${this.renderMobileSearch()} ${this.renderChangeListView()}
${this.renderDashboardView()} ${this.renderChangeView()}
@@ -386,6 +384,44 @@
<div class="errorMoreInfo">${this.lastError?.moreInfo}</div>
</div>
</main>
+ ${this.renderFooter()} ${this.renderKeyboardShortcutsDialog()}
+ ${this.renderRegistrationDialog()}
+ <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
+ <gr-error-manager
+ id="errorManager"
+ .loginUrl=${this.loginUrl}
+ ></gr-error-manager>
+ <gr-plugin-host id="plugins"></gr-plugin-host>
+ <gr-external-style
+ id="externalStyleForAll"
+ name="app-theme"
+ ></gr-external-style>
+ <gr-external-style
+ id="externalStyleForTheme"
+ name=${this.themeEndpoint}
+ ></gr-external-style>
+ `;
+ }
+
+ private renderHeader() {
+ if (this.hideHeaderAndFooter()) return nothing;
+ return html`
+ <gr-main-header
+ id="mainHeader"
+ .searchQuery=${(this.params as SearchViewState)?.query}
+ @mobile-search=${this.mobileSearchToggle}
+ @show-keyboard-shortcuts=${this.showKeyboardShortcuts}
+ .mobileSearchHidden=${!this.mobileSearch}
+ .loginUrl=${this.loginUrl}
+ ?aria-hidden=${this.footerHeaderAriaHidden}
+ >
+ </gr-main-header>
+ `;
+ }
+
+ private renderFooter() {
+ if (this.hideHeaderAndFooter()) return nothing;
+ return html`
<footer ?aria-hidden=${this.footerHeaderAriaHidden}>
<div>
Powered by
@@ -403,24 +439,16 @@
<gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
</div>
</footer>
- ${this.renderKeyboardShortcutsDialog()} ${this.renderRegistrationDialog()}
- <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
- <gr-error-manager
- id="errorManager"
- .loginUrl=${this.loginUrl}
- ></gr-error-manager>
- <gr-plugin-host id="plugins"></gr-plugin-host>
- <gr-external-style
- id="externalStyleForAll"
- name="app-theme"
- ></gr-external-style>
- <gr-external-style
- id="externalStyleForTheme"
- name=${this.themeEndpoint}
- ></gr-external-style>
`;
}
+ private hideHeaderAndFooter() {
+ return (
+ this.view === GerritView.PLUGIN_SCREEN &&
+ WHITE_LISTED_FULL_SCREEN_PLUGINS.includes(this.computePluginScreenName())
+ );
+ }
+
private renderMobileSearch() {
if (!this.mobileSearch) return nothing;
return html`
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index 4132c0e..da9c0c9 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -13,11 +13,16 @@
import {GrAnnotation} from '../embed/diff/gr-diff-highlight/gr-annotation';
import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context';
-import {initGerritPluginApi} from './shared/gr-js-api-interface/gr-gerrit';
-import {AppContext} from '../services/app-context';
+import {AppContext, injectAppContext} from '../services/app-context';
+import {Finalizable} from '../services/registry';
+import {PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
-export function initGlobalVariables(appContext: AppContext) {
+export function initGlobalVariables(appContext: AppContext & Finalizable) {
+ injectAppContext(appContext);
window.GrAnnotation = GrAnnotation;
window.GrPluginActionContext = GrPluginActionContext;
- initGerritPluginApi(appContext);
+}
+
+export function initGerrit(pluginLoader: PluginLoader) {
+ window.Gerrit = pluginLoader;
}
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 0cec8dd..4d0939ba 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -21,15 +21,21 @@
setCancelSyntheticClickEvents(false);
setPassiveTouchGestures(true);
-import {initGlobalVariables} from './gr-app-global-var-init';
+import {initGerrit, initGlobalVariables} from './gr-app-global-var-init';
import './gr-app-element';
import {Finalizable} from '../services/registry';
-import {provide} from '../models/dependency';
+import {
+ DependencyError,
+ DependencyToken,
+ provide,
+ Provider,
+} from '../models/dependency';
import {installPolymerResin} from '../scripts/polymer-resin-install';
import {
createAppContext,
createAppDependencies,
+ Creator,
} from '../services/app-context-init';
import {
initVisibilityReporter,
@@ -37,13 +43,14 @@
initErrorReporter,
initWebVitals,
} from '../services/gr-reporting/gr-reporting_impl';
-import {injectAppContext} from '../services/app-context';
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ServiceWorkerInstaller} from '../services/service-worker-installer';
+import {userModelToken} from '../models/user/user-model';
+import {pluginLoaderToken} from './shared/gr-js-api-interface/gr-plugin-loader';
const appContext = createAppContext();
-injectAppContext(appContext);
+initGlobalVariables(appContext);
const reportingService = appContext.reportingService;
initVisibilityReporter(reportingService);
initPerformanceReporter(reportingService);
@@ -60,15 +67,51 @@
override connectedCallback() {
super.connectedCallback();
- const dependencies = createAppDependencies(appContext);
- for (const [token, service] of dependencies) {
- this.finalizables.push(service);
- provide(this, token, () => service);
+ const dependencies = new Map<DependencyToken<unknown>, Provider<unknown>>();
+
+ const injectDependency = <T>(
+ token: DependencyToken<T>,
+ creator: Creator<T>
+ ) => {
+ let service: (T & Finalizable) | undefined = undefined;
+ dependencies.set(token, () => {
+ if (service) return service;
+ service = creator();
+ this.finalizables.push(service);
+ return service;
+ });
+ };
+
+ const resolver = <T>(token: DependencyToken<T>): T => {
+ const provider = dependencies.get(token);
+ if (provider) {
+ return provider() as T;
+ } else {
+ throw new DependencyError(
+ token,
+ 'Forgot to set up dependency for gr-app'
+ );
+ }
+ };
+
+ for (const [token, creator] of createAppDependencies(
+ appContext,
+ resolver
+ )) {
+ injectDependency(token, creator);
}
+ for (const [token, provider] of dependencies) {
+ provide(this, token, provider);
+ }
+
+ initGerrit(resolver(pluginLoaderToken));
+
+ // TODO(milutin): Move inside app dependencies.
if (!this.serviceWorkerInstaller) {
this.serviceWorkerInstaller = new ServiceWorkerInstaller(
appContext.flagsService,
- appContext.userModel
+ appContext.reportingService,
+ resolver(userModelToken)
);
}
}
@@ -91,5 +134,3 @@
'gr-app': GrApp;
}
}
-
-initGlobalVariables(appContext);
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index a87973b..730548c 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -16,7 +16,9 @@
createServerInfo,
} from '../test/test-data-generators';
import {GrAppElement} from './gr-app-element';
-import {GrRouter} from './core/gr-router/gr-router';
+import {GrRouter, routerToken} from './core/gr-router/gr-router';
+import {resolve} from '../models/dependency';
+import {removeRequestDependencyListener} from '../test/common-test-setup';
suite('gr-app tests', () => {
let grApp: GrApp;
@@ -34,9 +36,14 @@
stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
stubRestApi('getVersion').returns(Promise.resolve('42'));
stubRestApi('probePath').returns(Promise.resolve(false));
-
grApp = await fixture<GrApp>(html`<gr-app id="app"></gr-app>`);
- await grApp.updateComplete;
+ });
+
+ test('models resolve', () => {
+ // Verify that models resolve on grApp without falling back
+ // to the ones instantiated by the test-setup.
+ removeRequestDependencyListener();
+ assert.ok(resolve(grApp, routerToken)());
});
test('reporting', () => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
index da0035e..033df49 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
@@ -5,7 +5,7 @@
*/
import {EventType, PluginApi} from '../../../api/plugin';
import {AdminPluginApi, MenuLink} from '../../../api/admin';
-import {getAppContext} from '../../../services/app-context';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
/**
* GrAdminApi class.
@@ -16,9 +16,10 @@
// TODO(TS): maybe define as enum if its a limited set
private menuLinks: MenuLink[] = [];
- private readonly reporting = getAppContext().reportingService;
-
- constructor(private readonly plugin: PluginApi) {
+ constructor(
+ private readonly reporting: ReportingService,
+ private readonly plugin: PluginApi
+ ) {
this.reporting.trackApi(this.plugin, 'admin', 'constructor');
this.plugin.on(EventType.ADMIN_MENU_LINKS, this);
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.ts b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.ts
index da7aa9e..0d041d4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.ts
@@ -7,8 +7,9 @@
import {AdminPluginApi} from '../../../api/admin';
import {PluginApi} from '../../../api/plugin';
import '../../../test/common-test-setup';
+import {testResolver} from '../../../test/common-test-setup';
import '../../shared/gr-js-api-interface/gr-js-api-interface';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-admin-api tests', () => {
let adminApi: AdminPluginApi;
@@ -22,7 +23,7 @@
'0.1',
'http://test.com/plugins/testplugin/static/test.js'
);
- getPluginLoader().loadPlugins([]);
+ testResolver(pluginLoaderToken).loadPlugins([]);
adminApi = plugin.admin();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
index ab71255..bc4e701 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
@@ -5,17 +5,19 @@
*/
import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
import {PluginApi} from '../../../api/plugin';
-import {getAppContext} from '../../../services/app-context';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
export class GrAttributeHelper implements AttributeHelperPluginApi {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _promises = new Map<string, Promise<any>>();
- private readonly reporting = getAppContext().reportingService;
-
// TODO(TS): Change any to something more like HTMLElement.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- constructor(readonly plugin: PluginApi, public element: any) {
+ constructor(
+ private readonly reporting: ReportingService,
+ readonly plugin: PluginApi,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public element: any
+ ) {
this.reporting.trackApi(this.plugin, 'attribute', 'constructor');
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
index 4d59dc9..51cddbe 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
@@ -11,7 +11,8 @@
CheckResult,
CheckRun,
} from '../../../api/checks';
-import {getAppContext} from '../../../services/app-context';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {PluginsModel} from '../../../models/plugins/plugins-model';
const DEFAULT_CONFIG: ChecksApiConfig = {
fetchPollingIntervalSeconds: 60,
@@ -32,11 +33,11 @@
export class GrChecksApi implements ChecksPluginApi {
private state = State.NOT_REGISTERED;
- private readonly reporting = getAppContext().reportingService;
-
- private readonly pluginsModel = getAppContext().pluginsModel;
-
- constructor(readonly plugin: PluginApi) {
+ constructor(
+ private readonly reporting: ReportingService,
+ private readonly pluginsModel: PluginsModel,
+ readonly plugin: PluginApi
+ ) {
this.reporting.trackApi(this.plugin, 'checks', 'constructor');
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
index 011fbbf..54197ea 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
@@ -4,10 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {PluginApi} from '../../../api/plugin';
import {ChecksPluginApi} from '../../../api/checks';
import {assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-settings-api tests', () => {
let checksApi: ChecksPluginApi | undefined;
@@ -21,7 +22,7 @@
'0.1',
'http://test.com/plugins/testplugin/static/test.js'
);
- getPluginLoader().loadPlugins([]);
+ testResolver(pluginLoaderToken).loadPlugins([]);
assert.isOk(pluginApi);
checksApi = pluginApi!.checks();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts
index 61279cf..d5fc9f8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts
@@ -5,15 +5,13 @@
*/
import {html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
-import {
- getPluginEndpoints,
- ModuleInfo,
-} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {ModuleInfo} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {PluginApi} from '../../../api/plugin';
import {HookApi, PluginElement} from '../../../api/hook';
import {getAppContext} from '../../../services/app-context';
import {assertIsDefined} from '../../../utils/common-util';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {resolve} from '../../../models/dependency';
const INIT_PROPERTIES_TIMEOUT_MS = 10000;
@@ -38,6 +36,8 @@
private readonly reporting = getAppContext().reportingService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
override render() {
return html`<slot></slot>`;
}
@@ -45,12 +45,17 @@
override connectedCallback() {
super.connectedCallback();
assertIsDefined(this.name);
- getPluginEndpoints().onNewEndpoint(this.name, this.initModule);
- getPluginLoader()
+ this.getPluginLoader().pluginEndPoints.onNewEndpoint(
+ this.name,
+ this.initModule
+ );
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
assertIsDefined(this.name);
- const modules = getPluginEndpoints().getDetails(this.name);
+ const modules = this.getPluginLoader().pluginEndPoints.getDetails(
+ this.name
+ );
for (const module of modules) {
this.initModule(module);
}
@@ -62,7 +67,10 @@
domHook.handleInstanceDetached(el);
}
assertIsDefined(this.name);
- getPluginEndpoints().onDetachedEndpoint(this.name, this.initModule);
+ this.getPluginLoader().pluginEndPoints.onDetachedEndpoint(
+ this.name,
+ this.initModule
+ );
super.disconnectedCallback();
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts
index c3e6911..57888fa 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts
@@ -8,12 +8,7 @@
import '../gr-endpoint-param/gr-endpoint-param';
import '../gr-endpoint-slot/gr-endpoint-slot';
import {fixture, html, assert} from '@open-wc/testing';
-import {
- mockPromise,
- queryAndAssert,
- resetPlugins,
-} from '../../../test/test-utils';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {mockPromise, queryAndAssert} from '../../../test/test-utils';
import {GrEndpointDecorator} from './gr-endpoint-decorator';
import {PluginApi} from '../../../api/plugin';
import {GrEndpointParam} from '../gr-endpoint-param/gr-endpoint-param';
@@ -30,7 +25,6 @@
let banana: GrEndpointDecorator;
setup(async () => {
- resetPlugins();
container = await fixture(
html`<div>
<gr-endpoint-decorator name="first">
@@ -100,18 +94,11 @@
const replacementHookPromise = mockPromise();
replacementHook.onAttached(() => replacementHookPromise.resolve());
- // Mimic all plugins loaded.
- getPluginLoader().loadPlugins([]);
-
await decorationHookPromise;
await decorationHookSlotPromise;
await replacementHookPromise;
});
- teardown(() => {
- resetPlugins();
- });
-
test('imports plugin-provided modules into endpoints', () => {
const endpoints = Array.from(
container.querySelectorAll('gr-endpoint-decorator')
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
index 9915d4c..641d87b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
@@ -8,12 +8,14 @@
UnsubscribeCallback,
} from '../../../api/event-helper';
import {PluginApi} from '../../../api/plugin';
-import {getAppContext} from '../../../services/app-context';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
export class GrEventHelper implements EventHelperPluginApi {
- private readonly reporting = getAppContext().reportingService;
-
- constructor(readonly plugin: PluginApi, readonly element: HTMLElement) {
+ constructor(
+ private readonly reporting: ReportingService,
+ readonly plugin: PluginApi,
+ readonly element: HTMLElement
+ ) {
this.reporting.trackApi(this.plugin, 'event', 'constructor');
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts
index 43d9805..5a0e8df 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts
@@ -4,10 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin';
-import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {LitElement, html, PropertyValues} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {resolve} from '../../../models/dependency';
@customElement('gr-external-style')
export class GrExternalStyle extends LitElement {
@@ -20,6 +20,8 @@
stylesElements: HTMLElement[] = [];
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
override render() {
return html`<slot></slot>`;
}
@@ -29,7 +31,7 @@
// We remove all styles defined for different name.
this.removeStyles();
this.importAndApply();
- getPluginLoader()
+ this.getPluginLoader()
.awaitPluginsLoaded()
.then(() => this.importAndApply());
}
@@ -61,7 +63,9 @@
}
private importAndApply() {
- const moduleNames = getPluginEndpoints().getModules(this.name);
+ const moduleNames = this.getPluginLoader().pluginEndPoints.getModules(
+ this.name
+ );
for (const name of moduleNames) {
this.applyStyle(name);
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts
index ce87acb..6c0e789 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts
@@ -4,18 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
-import {mockPromise, MockPromise, resetPlugins} from '../../../test/test-utils';
+import {mockPromise, MockPromise} from '../../../test/test-utils';
import './gr-external-style';
import {GrExternalStyle} from './gr-external-style';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {PluginApi} from '../../../api/plugin';
import {fixture, html, assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-external-style integration tests', () => {
const TEST_URL = 'http://some.com/plugins/url.js';
let element: GrExternalStyle;
- let plugin: PluginApi;
+ let plugin: PluginApi | undefined;
let pluginsLoaded: MockPromise<void>;
let applyStyleSpy: sinon.SinonSpy;
@@ -46,7 +47,7 @@
const lateRegister = async () => {
installPlugin();
await createElement();
- plugin.registerStyleModule('foo', 'some-module');
+ plugin!.registerStyleModule('foo', 'some-module');
};
/**
@@ -54,17 +55,19 @@
*/
const earlyRegister = async () => {
installPlugin();
- plugin.registerStyleModule('foo', 'some-module');
+ plugin!.registerStyleModule('foo', 'some-module');
await createElement();
};
setup(() => {
pluginsLoaded = mockPromise();
- sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
+ sinon
+ .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
+ .returns(pluginsLoaded);
});
teardown(() => {
- resetPlugins();
+ plugin = undefined;
document.body
.querySelectorAll('custom-style')
.forEach(style => style.remove());
@@ -80,7 +83,7 @@
test('does not double apply', async () => {
await earlyRegister();
await element.updateComplete;
- plugin.registerStyleModule('foo', 'some-module');
+ plugin!.registerStyleModule('foo', 'some-module');
await element.updateComplete;
const stylesApplied = element.stylesApplied.filter(
name => name === 'some-module'
@@ -96,7 +99,7 @@
test('removes old custom-style if name is changed', async () => {
installPlugin();
- plugin.registerStyleModule('bar', 'some-module');
+ plugin!.registerStyleModule('bar', 'some-module');
await earlyRegister();
await element.updateComplete;
let customStyles = document.body.querySelectorAll('custom-style');
@@ -114,7 +117,7 @@
test('can apply more than one style', async () => {
await earlyRegister();
await element.updateComplete;
- plugin.registerStyleModule('foo', 'some-module2');
+ plugin!.registerStyleModule('foo', 'some-module2');
pluginsLoaded.resolve();
await element.updateComplete;
assert.strictEqual(element.stylesApplied.length, 2);
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index b0993b9..80765a8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -5,19 +5,20 @@
*/
import {LitElement} from 'lit';
import {customElement, state} from 'lit/decorators.js';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {ServerInfo} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@customElement('gr-plugin-host')
export class GrPluginHost extends LitElement {
@state()
config?: ServerInfo;
- // visible for testing
- readonly getConfigModel = resolve(this, configModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
constructor() {
super();
@@ -31,7 +32,10 @@
? [config.default_theme]
: [];
const instanceId = config?.gerrit?.instance_id;
- getPluginLoader().loadPlugins([...themes, ...jsPlugins], instanceId);
+ this.getPluginLoader().loadPlugins(
+ [...themes, ...jsPlugins],
+ instanceId
+ );
}
);
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
index bb89d12..0699e49 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
@@ -5,29 +5,39 @@
*/
import '../../../test/common-test-setup';
import './gr-plugin-host';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {GrPluginHost} from './gr-plugin-host';
import {fixture, html, assert} from '@open-wc/testing';
import {SinonStub} from 'sinon';
import {createServerInfo} from '../../../test/test-data-generators';
+import {
+ ConfigModel,
+ configModelToken,
+} from '../../../models/config/config-model';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
suite('gr-plugin-host tests', () => {
let element: GrPluginHost;
let loadPluginsStub: SinonStub;
+ let configModel: ConfigModel;
setup(async () => {
- loadPluginsStub = sinon.stub(getPluginLoader(), 'loadPlugins');
+ loadPluginsStub = sinon.stub(
+ testResolver(pluginLoaderToken),
+ 'loadPlugins'
+ );
element = await fixture<GrPluginHost>(html`
<gr-plugin-host></gr-plugin-host>
`);
await element.updateComplete;
+ configModel = testResolver(configModelToken);
sinon.stub(document.body, 'appendChild');
});
test('load plugins should be called', async () => {
loadPluginsStub.reset();
- element.getConfigModel().updateServerConfig({
+ configModel.updateServerConfig({
...createServerInfo(),
plugin: {
has_avatars: false,
@@ -46,7 +56,7 @@
test('theme plugins should be loaded if enabled', async () => {
loadPluginsStub.reset();
- element.getConfigModel().updateServerConfig({
+ configModel.updateServerConfig({
...createServerInfo(),
default_theme: 'gerrit-theme.js',
plugin: {
@@ -69,7 +79,7 @@
loadPluginsStub.reset();
const config = createServerInfo();
config.gerrit.instance_id = 'test-id';
- element.getConfigModel().updateServerConfig(config);
+ configModel.updateServerConfig(config);
assert.isTrue(loadPluginsStub.calledOnce);
assert.isTrue(loadPluginsStub.calledWith([], 'test-id'));
});
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 c9603f4..f63f34e 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
@@ -6,8 +6,11 @@
import '@polymer/iron-input/iron-input';
import '../../shared/gr-avatar/gr-avatar';
import '../../shared/gr-date-formatter/gr-date-formatter';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../../../styles/gr-form-styles';
import '../../../styles/shared-styles';
+import '../../shared/gr-account-chip/gr-account-chip';
+import '../../shared/gr-hovercard-account/gr-hovercard-account-contents';
import {AccountDetailInfo, ServerInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
@@ -75,6 +78,15 @@
div section.hide {
display: none;
}
+ gr-hovercard-account-contents {
+ display: block;
+ max-width: 600px;
+ margin-top: var(--spacing-m);
+ background: var(--dialog-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
`,
];
@@ -209,6 +221,23 @@
</iron-input>
</span>
</section>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ title="This is how you appear to others"
+ has-tooltip
+ show-icon
+ >
+ Account preview
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <gr-account-chip .account=${this.account}></gr-account-chip>
+ <gr-hovercard-account-contents
+ .account=${this.account}
+ ></gr-hovercard-account-contents>
+ </span>
+ </section>
</div>`;
}
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 518828a..f7e5dee 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
@@ -109,6 +109,21 @@
</iron-input>
</span>
</section>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip=""
+ show-icon=""
+ title="This is how you appear to others"
+ >
+ Account preview
+ </gr-tooltip-content>
+ </span>
+ <span class="value"
+ ><gr-account-chip></gr-account-chip>
+ <gr-hovercard-account-contents></gr-hovercard-account-contents>
+ </span>
+ </section>
</div>
`
);
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index f554ff0..183425d 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -7,7 +7,6 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
import {EditPreferencesInfo} from '../../../types/common';
-import {getAppContext} from '../../../services/app-context';
import {formStyles} from '../../../styles/gr-form-styles';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -15,6 +14,8 @@
import {customElement, query, state} from 'lit/decorators.js';
import {convertToString} from '../../../utils/string-util';
import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-edit-preferences')
export class GrEditPreferences extends LitElement {
@@ -46,13 +47,13 @@
@state() private originalEditPrefs?: EditPreferencesInfo;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
subscribe(
this,
- () => this.userModel.editPreferences$,
+ () => this.getUserModel().editPreferences$,
editPreferences => {
this.originalEditPrefs = editPreferences;
this.editPrefs = {...editPreferences};
@@ -307,7 +308,7 @@
async save() {
if (!this.editPrefs) return;
- await this.userModel.updateEditPreference(this.editPrefs);
+ await this.getUserModel().updateEditPreference(this.editPrefs);
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
index b2f8acd..32b32e2 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
@@ -6,10 +6,8 @@
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import '../../shared/gr-overlay/gr-overlay';
import {GpgKeyInfo, GpgKeyId} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {getAppContext} from '../../../services/app-context';
import {css, html, LitElement} from 'lit';
@@ -19,6 +17,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
import {fire} from '../../../utils/event-util';
+import {modalStyles} from '../../../styles/gr-modal-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -27,7 +26,7 @@
}
@customElement('gr-gpg-editor')
export class GrGpgEditor extends LitElement {
- @query('#viewKeyOverlay') viewKeyOverlay?: GrOverlay;
+ @query('#viewKeyModal') viewKeyModal?: HTMLDialogElement;
@query('#addButton') addButton?: GrButton;
@@ -53,6 +52,7 @@
static override styles = [
formStyles,
sharedStyles,
+ modalStyles,
css`
.keyHeader {
width: 9em;
@@ -60,7 +60,7 @@
.userIdHeader {
width: 15em;
}
- #viewKeyOverlay {
+ #viewKeyModal {
padding: var(--spacing-xxl);
width: 50em;
}
@@ -97,7 +97,7 @@
${this.keys.map((key, index) => this.renderKey(key, index))}
</tbody>
</table>
- <gr-overlay id="viewKeyOverlay" with-backdrop="">
+ <dialog id="viewKeyModal" tabindex="-1">
<fieldset>
<section>
<span class="title">Status</span>
@@ -111,11 +111,11 @@
<gr-button
class="closeButton"
@click=${() => {
- this.viewKeyOverlay?.close();
+ this.viewKeyModal?.close();
}}
>Close</gr-button
>
- </gr-overlay>
+ </dialog>
<gr-button @click=${this.save} ?disabled=${!this.hasUnsavedChanges}
>Save changes</gr-button
>
@@ -201,7 +201,7 @@
private showKey(key: GpgKeyInfo) {
this.keyToView = key;
- this.viewKeyOverlay?.open();
+ this.viewKeyModal?.showModal();
}
private handleNewKeyChanged(e: BindValueChangeEvent) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
index 8e653fc..5be5b29 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
@@ -138,13 +138,7 @@
</tr>
</tbody>
</table>
- <gr-overlay
- aria-hidden="true"
- id="viewKeyOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="viewKeyModal" tabindex="-1">
<fieldset>
<section>
<span class="title"> Status </span> <span class="value"> </span>
@@ -161,7 +155,7 @@
>
Close
</gr-button>
- </gr-overlay>
+ </dialog>
<gr-button
aria-disabled="true"
disabled=""
@@ -242,7 +236,7 @@
});
test('show key', () => {
- const openSpy = sinon.spy(element.viewKeyOverlay!, 'open');
+ const openSpy = sinon.spy(element.viewKeyModal!, 'showModal');
// Get the show button for the last row.
const button = queryAndAssert<GrButton>(
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
index 9595391..16e262b 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
@@ -5,13 +5,12 @@
*/
import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import '../../shared/gr-overlay/gr-overlay';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, property, query} from 'lit/decorators.js';
+import {modalStyles} from '../../../styles/gr-modal-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -21,8 +20,8 @@
@customElement('gr-http-password')
export class GrHttpPassword extends LitElement {
- @query('#generatedPasswordOverlay')
- generatedPasswordOverlay?: GrOverlay;
+ @query('#generatedPasswordModal')
+ generatedPasswordModal?: HTMLDialogElement;
@property({type: String})
_username?: string;
@@ -68,13 +67,14 @@
return [
sharedStyles,
formStyles,
+ modalStyles,
css`
.password {
font-family: var(--monospace-font-family);
font-size: var(--font-size-mono);
line-height: var(--line-height-mono);
}
- #generatedPasswordOverlay {
+ #generatedPasswordModal {
padding: var(--spacing-xxl);
width: 50em;
}
@@ -120,10 +120,10 @@
(opens in a new tab)
</span>
</div>
- <gr-overlay
- id="generatedPasswordOverlay"
- @iron-overlay-closed=${this._generatedPasswordOverlayClosed}
- with-backdrop
+ <dialog
+ tabindex="-1"
+ id="generatedPasswordModal"
+ @closed=${this._generatedPasswordModalClosed}
>
<div class="gr-form-styles">
<section id="generatedPasswordDisplay">
@@ -141,26 +141,26 @@
This password will not be displayed again.<br />
If you lose it, you will need to generate a new one.
</section>
- <gr-button link="" class="closeButton" @click=${this._closeOverlay}
+ <gr-button link="" class="closeButton" @click=${this._closeModal}
>Close</gr-button
>
</div>
- </gr-overlay>`;
+ </dialog>`;
}
_handleGenerateTap() {
this._generatedPassword = 'Generating...';
- this.generatedPasswordOverlay?.open();
+ this.generatedPasswordModal?.showModal();
this.restApiService.generateAccountHttpPassword().then(newPassword => {
this._generatedPassword = newPassword;
});
}
- _closeOverlay() {
- this.generatedPasswordOverlay?.close();
+ _closeModal() {
+ this.generatedPasswordModal?.close();
}
- _generatedPasswordOverlayClosed() {
+ _generatedPasswordModalClosed() {
this._generatedPassword = '';
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
index 116d349..a582044 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
@@ -57,13 +57,7 @@
(opens in a new tab)
</span>
</div>
- <gr-overlay
- aria-hidden="true"
- id="generatedPasswordOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog tabindex="-1" id="generatedPasswordModal">
<div class="gr-form-styles">
<section id="generatedPasswordDisplay">
<span class="title"> New Password: </span>
@@ -90,7 +84,7 @@
Close
</gr-button>
</div>
- </gr-overlay>
+ </dialog>
`
);
});
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
index 4f5411d..7f67ea8 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
@@ -5,10 +5,8 @@
*/
import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog';
import '../../shared/gr-button/gr-button';
-import '../../shared/gr-overlay/gr-overlay';
import {getBaseUrl} from '../../../utils/url-util';
import {AccountExternalIdInfo, ServerInfo} from '../../../types/common';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
import {AuthType} from '../../../constants/constants';
import {LitElement, css, html, PropertyValues} from 'lit';
@@ -18,12 +16,13 @@
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
import {assertIsDefined} from '../../../utils/common-util';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const AUTH = [AuthType.OPENID, AuthType.OAUTH];
@customElement('gr-identities')
export class GrIdentities extends LitElement {
- @query('#overlay') overlay?: GrOverlay;
+ @query('#modal') modal?: HTMLDialogElement;
@state() private identities: AccountExternalIdInfo[] = [];
@@ -40,6 +39,7 @@
static override styles = [
sharedStyles,
formStyles,
+ modalStyles,
css`
tr th.emailAddressHeader,
tr th.identityHeader {
@@ -98,7 +98,7 @@
</fieldset>`
)}
</div>
- <gr-overlay id="overlay" with-backdrop>
+ <dialog id="modal" tabindex="-1">
<gr-confirm-delete-item-dialog
class="confirmDialog"
@confirm=${this.handleDeleteItemConfirm}
@@ -106,7 +106,7 @@
.item=${this.idName}
itemtypename="ID"
></gr-confirm-delete-item-dialog>
- </gr-overlay>`;
+ </dialog>`;
}
private renderIdentity(account: AccountExternalIdInfo, index: number) {
@@ -156,7 +156,7 @@
}
handleDeleteItemConfirm() {
- this.overlay?.close();
+ this.modal?.close();
assertIsDefined(this.idName);
return this.restApiService.deleteAccountIdentity([this.idName]).then(() => {
this.loadData();
@@ -164,12 +164,12 @@
}
private handleConfirmDialogCancel() {
- this.overlay?.close();
+ this.modal?.close();
}
private handleDeleteItem(name: string) {
this.idName = name;
- this.overlay?.open();
+ this.modal?.showModal();
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
index 84df178..d52b423 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
@@ -7,7 +7,7 @@
import './gr-identities';
import {GrIdentities} from './gr-identities';
import {AuthType} from '../../../constants/constants';
-import {stubRestApi} from '../../../test/test-utils';
+import {stubRestApi, waitUntilVisible} from '../../../test/test-utils';
import {ServerInfo} from '../../../types/common';
import {createServerInfo} from '../../../test/test-data-generators';
import {queryAll, queryAndAssert} from '../../../test/test-utils';
@@ -96,19 +96,13 @@
</table>
</fieldset>
</div>
- <gr-overlay
- aria-hidden="true"
- id="overlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="modal" tabindex="-1">
<gr-confirm-delete-item-dialog
class="confirmDialog"
itemtypename="ID"
>
- </gr-confirm-delete-item-dialog
- ></gr-overlay>`
+ </gr-confirm-delete-item-dialog>
+ </dialog>`
);
});
@@ -150,7 +144,7 @@
const deleteBtn = queryAndAssert<GrButton>(element, '.deleteButton');
deleteBtn.click();
await element.updateComplete;
- assert.isTrue(element.overlay?.opened);
+ await waitUntilVisible(element.modal!);
});
test('computeShowLinkAnotherIdentity', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index 460cc7c..9c23857 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -12,12 +12,13 @@
import {state, customElement} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {subscribe} from '../../lit/subscription-controller';
-import {getAppContext} from '../../../services/app-context';
import {deepEqual} from '../../../utils/deep-util';
import {createDefaultPreferences} from '../../../constants/constants';
import {fontStyles} from '../../../styles/gr-font-styles';
import {classMap} from 'lit/directives/class-map.js';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
@customElement('gr-menu-editor')
export class GrMenuEditor extends LitElement {
@@ -33,13 +34,13 @@
@state()
newUrl = '';
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
this.originalPrefs = prefs;
this.menuItems = [...prefs.my];
@@ -196,7 +197,7 @@
}
private handleSave() {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
...this.originalPrefs,
my: this.menuItems,
});
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index ff2904a..92b1f86 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -63,6 +63,7 @@
import {resolve} from '../../../models/dependency';
import {settingsViewModelToken} from '../../../models/views/settings';
import {areNotificationsEnabled} from '../../../utils/worker-util';
+import {userModelToken} from '../../../models/user/user-model';
const GERRIT_DOCS_BASE_URL =
'https://gerrit-review.googlesource.com/' + 'Documentation';
@@ -201,7 +202,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
// private but used in test
readonly flagsService = getAppContext().flagsService;
@@ -220,14 +221,14 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
acc => {
this.account = acc;
}
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
if (!prefs) {
throw new Error('getPreferences returned undefined');
@@ -1148,7 +1149,7 @@
// private but used in test
handleSavePreferences() {
- return this.userModel.updatePreferences(this.localPrefs);
+ return this.getUserModel().updatePreferences(this.localPrefs);
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
index a73170a..17ac2a6 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
@@ -10,7 +10,6 @@
import {SshKeyInfo} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
import {LitElement, css, html, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
@@ -18,6 +17,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {fire} from '../../../utils/event-util';
import {BindValueChangeEvent} from '../../../types/events';
+import {modalStyles} from '../../../styles/gr-modal-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -47,7 +47,7 @@
@query('#newKey') newKeyEditor!: IronAutogrowTextareaElement;
- @query('#viewKeyOverlay') viewKeyOverlay!: GrOverlay;
+ @query('#viewKeyModal') viewKeyModal!: HTMLDialogElement;
private readonly restApiService = getAppContext().restApiService;
@@ -55,6 +55,7 @@
return [
formStyles,
sharedStyles,
+ modalStyles,
css`
.statusHeader {
width: 4em;
@@ -62,7 +63,7 @@
.keyHeader {
width: 7.5em;
}
- #viewKeyOverlay {
+ #viewKeyModal {
padding: var(--spacing-xxl);
width: 50em;
}
@@ -121,7 +122,7 @@
${this.keys.map((key, index) => this.renderKey(key, index))}
</tbody>
</table>
- <gr-overlay id="viewKeyOverlay" with-backdrop="">
+ <dialog id="viewKeyModal" tabindex="-1">
<fieldset>
<section>
<span class="title">Algorithm</span>
@@ -140,10 +141,10 @@
</fieldset>
<gr-button
class="closeButton"
- @click=${() => this.viewKeyOverlay.close()}
+ @click=${() => this.viewKeyModal.close()}
>Close</gr-button
>
- </gr-overlay>
+ </dialog>
<gr-button
@click=${() => this.save()}
?disabled=${!this.hasUnsavedChanges}
@@ -231,7 +232,7 @@
const el = e.target as GrButton;
const index = Number(el.getAttribute('data-index')!);
this.keyToView = this.keys[index];
- this.viewKeyOverlay.open();
+ this.viewKeyModal.showModal();
}
private handleDeleteKey(e: Event) {
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
index c5641ff..9528fb2 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
@@ -127,13 +127,7 @@
</tr>
</tbody>
</table>
- <gr-overlay
- aria-hidden="true"
- id="viewKeyOverlay"
- style="outline: none; display: none;"
- tabindex="-1"
- with-backdrop=""
- >
+ <dialog id="viewKeyModal" tabindex="-1">
<fieldset>
<section>
<span class="title"> Algorithm </span>
@@ -156,7 +150,7 @@
>
Close
</gr-button>
- </gr-overlay>
+ </dialog>
<gr-button
aria-disabled="true"
disabled=""
@@ -227,7 +221,7 @@
});
test('show key', () => {
- const openSpy = sinon.spy(element.viewKeyOverlay, 'open');
+ const openSpy = sinon.spy(element.viewKeyModal, 'showModal');
// Get the show button for the last row.
const button = query<GrButton>(
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
index fb38b59..15ec512 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
@@ -193,12 +193,12 @@
// private but used in tests.
getProjectSuggestions(input: string) {
- return this.restApiService.getSuggestedProjects(input).then(response => {
- const projects: AutocompleteSuggestion[] = [];
- for (const [name, project] of Object.entries(response ?? {})) {
- projects.push({name, value: project.id});
+ return this.restApiService.getSuggestedRepos(input).then(response => {
+ const repos: AutocompleteSuggestion[] = [];
+ for (const [name, repo] of Object.entries(response ?? {})) {
+ repos.push({name, value: repo.id});
}
- return projects;
+ return repos;
});
}
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
index 1280d6e..c608656 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
@@ -44,7 +44,7 @@
] as ProjectWatchInfo[];
stubRestApi('getWatchedProjects').returns(Promise.resolve(projects));
- suggestionStub = stubRestApi('getSuggestedProjects').callsFake(input => {
+ suggestionStub = stubRestApi('getSuggestedRepos').callsFake(input => {
if (input.startsWith('th')) {
return Promise.resolve({
'the project': {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index aa5fd58e..bb0200a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -22,6 +22,8 @@
import {getRemovedByIconClickReason} from '../../../utils/attention-set-util';
import {ifDefined} from 'lit/directives/if-defined.js';
import {createSearchUrl} from '../../../models/views/search';
+import {accountsModelToken} from '../../../models/accounts-model/accounts-model';
+import {resolve} from '../../../models/dependency';
@customElement('gr-account-label')
export class GrAccountLabel extends LitElement {
@@ -97,7 +99,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly accountsModel = getAppContext().accountsModel;
+ private readonly getAccountsModel = resolve(this, accountsModelToken);
static override get styles() {
return [
@@ -190,7 +192,7 @@
override async updated() {
assertIsDefined(this.account, 'account');
- const account = await this.accountsModel.fillDetails(this.account);
+ const account = await this.getAccountsModel().fillDetails(this.account);
if (account) this.account = account;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index bc64647..65d8859 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -17,7 +17,7 @@
SuggestedReviewerInfo,
isGroup,
} from '../../../types/common';
-import {ReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {ReviewerSuggestionsProvider} from '../../../services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
import {fire, fireAlert} from '../../../utils/event-util';
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 3bf7fb0..6b4d670 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -26,7 +26,7 @@
queryAndAssert,
waitUntil,
} from '../../../test/test-utils';
-import {ReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {ReviewerSuggestionsProvider} from '../../../services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import {
AutocompleteSuggestion,
GrAutocomplete,
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index 9b80282..9342715 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -5,7 +5,6 @@
*/
import '../gr-button/gr-button';
import '../../../styles/shared-styles';
-import {getRootElement} from '../../../scripts/rootElement';
import {ErrorType} from '../../../types/types';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@@ -170,14 +169,14 @@
this.actionText = actionText;
this._hideActionButton = !actionText;
this._actionCallback = actionCallback;
- getRootElement().appendChild(this);
+ document.body.appendChild(this);
this.shown = true;
}
hide() {
this.shown = false;
if (this._hasZeroTransitionDuration()) {
- getRootElement().removeChild(this);
+ document.body.removeChild(this);
}
}
@@ -197,7 +196,7 @@
return;
}
- getRootElement().removeChild(this);
+ document.body.removeChild(this);
}
_handleActionTap(e: MouseEvent) {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
index b34724c..1ea2a64 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -4,11 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {getBaseUrl} from '../../../utils/url-util';
-import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
import {AccountInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
import {LitElement, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
+import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
+import {resolve} from '../../../models/dependency';
/**
* The <gr-avatar> component works by updating its own background and visibility
@@ -26,6 +27,8 @@
private readonly restApiService = getAppContext().restApiService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
static override get styles() {
return [
css`
@@ -54,7 +57,7 @@
super.connectedCallback();
Promise.all([
this.restApiService.getConfig(),
- getPluginLoader().awaitPluginsLoaded(),
+ this.getPluginLoader().awaitPluginsLoaded(),
]).then(([cfg]) => {
this.hasAvatars = Boolean(cfg?.plugin?.has_avatars);
this.updateHostVisibilityAndImage();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index f33f7d8..75974e5 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -115,23 +115,6 @@
var(--background-color);
}
- /* Some mobile browsers treat focused element as hovered element.
- As a result, element remains hovered after click (has grey background in default theme).
- Use @media (hover:none) to remove background if
- user's primary input mechanism can't hover over elements.
- See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
-
- Note 1: not all browsers support this media query
- (see https://caniuse.com/#feat=css-media-interaction).
- If browser doesn't support it, then the whole content of @media .. is ignored.
- This is why the default behavior is placed outside of @media.
- */
- @media (hover: none) {
- paper-button:hover {
- background: transparent;
- }
- }
-
:host([primary]) {
--background-color: var(--primary-button-background-color);
--text-color: var(--primary-button-text-color);
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 9dfbc04..df41b1f4 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
@@ -74,6 +74,8 @@
import {HtmlPatched} from '../../../utils/lit-util';
import {createDiffUrl} from '../../../models/views/diff';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
+import {highlightServiceToken} from '../../../services/highlight/highlight-service';
declare global {
interface HTMLElementEventMap {
@@ -247,18 +249,20 @@
@state()
saving = false;
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly reporting = getAppContext().reportingService;
private readonly shortcuts = new ShortcutController(this);
- private readonly syntaxLayer = new GrSyntaxLayerWorker();
+ private readonly syntaxLayer = new GrSyntaxLayerWorker(
+ resolve(this, highlightServiceToken),
+ () => getAppContext().reportingService
+ );
// for COMMENTS_AUTOCLOSE logging purposes only
readonly uid = performance.now().toString(36) + Math.random().toString(36);
@@ -281,7 +285,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
@@ -291,12 +295,12 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
x => this.syntaxLayer.setEnabled(!!x.syntax_highlighting)
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
const layers: DiffLayer[] = [this.syntaxLayer];
if (!prefs.disable_token_highlighting) {
@@ -307,7 +311,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
prefs => {
this.prefs = {
...prefs,
@@ -741,7 +745,7 @@
if (this.isNewThread()) return undefined;
return createDiffUrl({
changeNum: this.changeNum,
- project: this.repoName,
+ repo: this.repoName,
path: this.thread.path,
patchNum: this.thread.patchNum,
});
@@ -770,7 +774,7 @@
assertIsDefined(this.rootId, 'rootId of comment thread');
return createDiffUrl({
changeNum: this.changeNum,
- project: this.repoName,
+ repo: this.repoName,
commentId: this.rootId,
});
}
@@ -784,13 +788,13 @@
if (this.isPatchsetLevel()) {
url = createChangeUrl({
changeNum: this.changeNum,
- project: this.repoName,
+ repo: this.repoName,
commentId: comment.id,
});
} else {
url = createDiffUrl({
changeNum: this.changeNum,
- project: this.repoName,
+ repo: this.repoName,
commentId: comment.id,
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 10e54c7..97eafc9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -30,6 +30,8 @@
import {GrButton} from '../gr-button/gr-button';
import {SpecialFilePath} from '../../../constants/constants';
import {GrIcon} from '../gr-icon/gr-icon';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {testResolver} from '../../../test/common-test-setup';
const c1 = {
author: {name: 'Kermit'},
@@ -311,7 +313,7 @@
setup(async () => {
savePromise = mockPromise<DraftInfo>();
stub = sinon
- .stub(element.getCommentsModel(), 'saveDraft')
+ .stub(testResolver(commentsModelToken), 'saveDraft')
.returns(savePromise);
element.thread = createThread(c1, {...c2, unresolved: true});
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 9e8264c..d9004ae 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -11,7 +11,6 @@
import '../gr-dialog/gr-dialog';
import '../gr-formatted-text/gr-formatted-text';
import '../gr-icon/gr-icon';
-import '../gr-overlay/gr-overlay';
import '../gr-textarea/gr-textarea';
import '../gr-tooltip-content/gr-tooltip-content';
import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
@@ -21,7 +20,6 @@
import {customElement, property, query, state} from 'lit/decorators.js';
import {resolve} from '../../../models/dependency';
import {GrTextarea} from '../gr-textarea/gr-textarea';
-import {GrOverlay} from '../gr-overlay/gr-overlay';
import {
AccountDetailInfo,
NumericChangeId,
@@ -49,7 +47,7 @@
} from '../../../types/events';
import {fire, fireEvent} from '../../../utils/event-util';
import {assertIsDefined, assert} from '../../../utils/common-util';
-import {Key, Modifier} from '../../../utils/dom-util';
+import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {sharedStyles} from '../../../styles/shared-styles';
import {subscribe} from '../../lit/subscription-controller';
@@ -64,6 +62,8 @@
import {KnownExperimentId} from '../../../services/flags/flags';
import {isBase64FileContent} from '../../../api/rest-api';
import {createDiffUrl} from '../../../models/views/diff';
+import {userModelToken} from '../../../models/user/user-model';
+import {modalStyles} from '../../../styles/gr-modal-styles';
const UNSAVED_MESSAGE = 'Unable to save draft';
@@ -128,8 +128,8 @@
@query('#resolvedCheckbox')
resolvedCheckbox?: HTMLInputElement;
- @query('#confirmDeleteOverlay')
- confirmDeleteOverlay?: GrOverlay;
+ @query('#confirmDeleteModal')
+ confirmDeleteModal?: HTMLDialogElement;
@property({type: Object})
comment?: Comment;
@@ -201,7 +201,7 @@
unresolved = true;
@property({type: Boolean})
- showConfirmDeleteOverlay = false;
+ showConfirmDeleteModal = false;
@property({type: Boolean})
unableToSave = false;
@@ -229,10 +229,9 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly shortcuts = new ShortcutController(this);
@@ -282,12 +281,12 @@
}
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
this,
- () => this.userModel.isAdmin$,
+ () => this.getUserModel().isAdmin$,
x => (this.isAdmin = x)
);
@@ -330,6 +329,7 @@
static override get styles() {
return [
sharedStyles,
+ modalStyles,
css`
:host {
display: block;
@@ -513,6 +513,13 @@
</gr-endpoint-param>
<gr-endpoint-param name="editing" .value=${this.editing}>
</gr-endpoint-param>
+ <gr-endpoint-param name="message" .value=${this.messageText}>
+ </gr-endpoint-param>
+ <gr-endpoint-param
+ name="isDraft"
+ .value=${isDraftOrUnsaved(this.comment)}
+ >
+ </gr-endpoint-param>
<div id="container" class=${classMap(classes)}>
${this.renderHeader()}
<div class="body">
@@ -638,7 +645,7 @@
title="Delete Comment"
link
class="action delete"
- @click=${this.openDeleteCommentOverlay}
+ @click=${this.openDeleteCommentModal}
>
<gr-icon id="icon" icon="delete" filled></gr-icon>
</gr-button>
@@ -811,7 +818,8 @@
// fixed. Currently diff line doesn't match commit message line, because
// of metadata in diff, which aren't in content api request.
if (this.comment.path === SpecialFilePath.COMMIT_MESSAGE) return nothing;
- if (this.isOwner) return nothing;
+ // TODO(milutin): disable user suggestions for owners, after user study.
+ // if (this.isOwner) return nothing;
return html`<gr-button
link
class="action suggestEdit"
@@ -930,16 +938,16 @@
}
private renderConfirmDialog() {
- if (!this.showConfirmDeleteOverlay) return;
+ if (!this.showConfirmDeleteModal) return;
return html`
- <gr-overlay id="confirmDeleteOverlay" with-backdrop>
+ <dialog id="confirmDeleteModal" tabindex="-1">
<gr-confirm-delete-comment-dialog
id="confirmDeleteComment"
@confirm=${this.handleConfirmDeleteComment}
- @cancel=${this.closeDeleteCommentOverlay}
+ @cancel=${this.closeDeleteCommentModal}
>
</gr-confirm-delete-comment-dialog>
- </gr-overlay>
+ </dialog>
`;
}
@@ -949,7 +957,7 @@
if (!comment.id) throw new Error('comment must have an id');
return createDiffUrl({
changeNum: this.changeNum,
- project: this.repoName,
+ repo: this.repoName,
commentId: comment.id,
});
}
@@ -974,6 +982,14 @@
}
}
+ override updated(changed: PropertyValues) {
+ if (changed.has('editing')) {
+ if (this.editing && !this.permanentEditingMode) {
+ whenVisible(this, () => this.textarea?.putCursorAtEnd());
+ }
+ }
+ }
+
override willUpdate(changed: PropertyValues) {
this.firstWillUpdate();
if (changed.has('editing')) {
@@ -1062,7 +1078,6 @@
this.unresolved = this.comment?.unresolved ?? true;
this.originalMessage = this.messageText;
this.originalUnresolved = this.unresolved;
- setTimeout(() => this.textarea?.putCursorAtEnd(), 1);
}
// Parent components such as the reply dialog might be interested in whether
@@ -1080,6 +1095,10 @@
return !this.messageText?.trimEnd();
}
+ override focus() {
+ this.textarea?.focus();
+ }
+
private handleEsc() {
// vim users don't like ESC to cancel/discard, so only do this when the
// comment text is empty.
@@ -1243,16 +1262,16 @@
}
}
- private async openDeleteCommentOverlay() {
- this.showConfirmDeleteOverlay = true;
+ private async openDeleteCommentModal() {
+ this.showConfirmDeleteModal = true;
await this.updateComplete;
- await this.confirmDeleteOverlay?.open();
+ await this.confirmDeleteModal?.showModal();
}
- private closeDeleteCommentOverlay() {
- this.showConfirmDeleteOverlay = false;
- this.confirmDeleteOverlay?.remove();
- this.confirmDeleteOverlay?.close();
+ private closeDeleteCommentModal() {
+ this.showConfirmDeleteModal = false;
+ this.confirmDeleteModal?.remove();
+ this.confirmDeleteModal?.close();
}
/**
@@ -1265,7 +1284,7 @@
*/
// private, but visible for testing
handleConfirmDeleteComment() {
- const dialog = this.confirmDeleteOverlay?.querySelector(
+ const dialog = this.confirmDeleteModal?.querySelector(
'#confirmDeleteComment'
) as GrConfirmDeleteCommentDialog | null;
if (!dialog || !dialog.message) {
@@ -1285,7 +1304,7 @@
dialog.message
)
.then(newComment => {
- this.closeDeleteCommentOverlay();
+ this.closeDeleteCommentModal();
this.comment = newComment;
});
}
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 2293619..5b191c8 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
@@ -47,9 +47,15 @@
import {SinonStub} from 'sinon';
import {fixture, html, assert} from '@open-wc/testing';
import {GrButton} from '../gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
suite('gr-comment tests', () => {
let element: GrComment;
+ let commentsModel: CommentsModel;
const account = {
email: 'dhruvsri@google.com' as EmailAddress,
name: 'Dhruv Srivastava',
@@ -77,6 +83,7 @@
.comment=${comment}
></gr-comment>`
);
+ commentsModel = testResolver(commentsModelToken);
});
suite('DOM rendering', () => {
@@ -95,6 +102,8 @@
<gr-endpoint-decorator name="comment">
<gr-endpoint-param name="comment"></gr-endpoint-param>
<gr-endpoint-param name="editing"></gr-endpoint-param>
+ <gr-endpoint-param name="message"></gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"></gr-endpoint-param>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -131,6 +140,8 @@
<gr-endpoint-decorator name="comment">
<gr-endpoint-param name="comment"></gr-endpoint-param>
<gr-endpoint-param name="editing"></gr-endpoint-param>
+ <gr-endpoint-param name="message"></gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"></gr-endpoint-param>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -169,6 +180,8 @@
<gr-endpoint-decorator name="comment">
<gr-endpoint-param name="comment"></gr-endpoint-param>
<gr-endpoint-param name="editing"></gr-endpoint-param>
+ <gr-endpoint-param name="message"></gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"></gr-endpoint-param>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -261,6 +274,8 @@
<gr-endpoint-decorator name="comment">
<gr-endpoint-param name="comment"></gr-endpoint-param>
<gr-endpoint-param name="editing"></gr-endpoint-param>
+ <gr-endpoint-param name="message"></gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"></gr-endpoint-param>
<div class="container draft" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -336,6 +351,8 @@
<gr-endpoint-decorator name="comment">
<gr-endpoint-param name="comment"></gr-endpoint-param>
<gr-endpoint-param name="editing"></gr-endpoint-param>
+ <gr-endpoint-param name="message"></gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"></gr-endpoint-param>
<div class="container draft" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -483,9 +500,9 @@
deleteButton.click();
await element.updateComplete;
- assertIsDefined(element.confirmDeleteOverlay, 'confirmDeleteOverlay');
+ assertIsDefined(element.confirmDeleteModal, 'confirmDeleteModal');
const dialog = queryAndAssert<GrConfirmDeleteCommentDialog>(
- element.confirmDeleteOverlay,
+ element.confirmDeleteModal,
'#confirmDeleteComment'
);
dialog.message = 'removal reason';
@@ -550,9 +567,7 @@
test('save', async () => {
const savePromise = mockPromise<DraftInfo>();
- const stub = sinon
- .stub(element.getCommentsModel(), 'saveDraft')
- .returns(savePromise);
+ const stub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
element.comment = createDraft();
element.editing = true;
@@ -597,7 +612,7 @@
test('save failed', async () => {
sinon
- .stub(element.getCommentsModel(), 'saveDraft')
+ .stub(commentsModel, 'saveDraft')
.returns(Promise.reject(new Error('saving failed')));
element.comment = createDraft();
@@ -617,7 +632,7 @@
test('discard', async () => {
const discardPromise = mockPromise<void>();
const stub = sinon
- .stub(element.getCommentsModel(), 'discardDraft')
+ .stub(commentsModel, 'discardDraft')
.returns(discardPromise);
element.comment = createDraft();
@@ -640,7 +655,7 @@
});
test('resolved comment state indicated by checkbox', async () => {
- const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
+ const saveStub = sinon.stub(commentsModel, 'saveDraft');
element.comment = {
...createComment(),
__draft: true,
@@ -664,11 +679,8 @@
});
test('saving empty text calls discard()', async () => {
- const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
- const discardStub = sinon.stub(
- element.getCommentsModel(),
- 'discardDraft'
- );
+ const saveStub = sinon.stub(commentsModel, 'saveDraft');
+ const discardStub = sinon.stub(commentsModel, 'discardDraft');
element.comment = createDraft();
element.editing = true;
await element.updateComplete;
@@ -742,9 +754,7 @@
setup(async () => {
clock = sinon.useFakeTimers();
savePromise = mockPromise<DraftInfo>();
- saveStub = sinon
- .stub(element.getCommentsModel(), 'saveDraft')
- .returns(savePromise);
+ saveStub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
element.comment = createUnsaved();
element.editing = true;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 0e9b874..350aa7f 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -105,7 +105,8 @@
link=""
class="copyToClipboard"
@click=${this._copyToClipboard}
- aria-label="Click to copy to clipboard"
+ aria-label="copy"
+ aria-description="Click to copy to clipboard"
>
<div>
<gr-icon id="icon" icon="content_copy" small></gr-icon>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index 6e93803..4c36a56 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -42,7 +42,8 @@
<gr-tooltip-content>
<gr-button
aria-disabled="false"
- aria-label="Click to copy to clipboard"
+ aria-label="copy"
+ aria-description="Click to copy to clipboard"
class="copyToClipboard"
id="copy-clipboard-button"
link=""
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts
similarity index 85%
rename from polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
rename to polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts
index 4b8157e..81d2b45 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts
@@ -4,18 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
-// eslint-disable-next-line import/named
import {fixture, html, assert} from '@open-wc/testing';
import {AbortStop, CursorMoveResult} from '../../../api/core';
import {GrCursorManager} from './gr-cursor-manager';
suite('gr-cursor-manager tests', () => {
- let cursor;
- let list;
+ let cursor: GrCursorManager;
+ let list: Element;
setup(async () => {
- list = await fixture(html`
- <ul>
+ list = await fixture(html` <ul>
<li>A</li>
<li>B</li>
<li>C</li>
@@ -42,11 +40,11 @@
assert.isNotOk(cursor.target);
// Select the third stop.
- cursor.setCursor(list.children[2]);
+ cursor.setCursor(list.children[2] as HTMLElement);
// It should update its internal state and update the element's class.
assert.equal(cursor.index, 2);
- assert.equal(cursor.target, list.children[2]);
+ assert.equal(cursor.target, list.children[2] as HTMLElement);
assert.isTrue(list.children[2].classList.contains('targeted'));
assert.isFalse(cursor.isAtStart());
assert.isFalse(cursor.isAtEnd());
@@ -58,7 +56,7 @@
// unselected.
assert.equal(result, CursorMoveResult.MOVED);
assert.equal(cursor.index, 3);
- assert.equal(cursor.target, list.children[3]);
+ assert.equal(cursor.target, list.children[3] as HTMLElement);
assert.isTrue(cursor.isAtEnd());
assert.isFalse(list.children[2].classList.contains('targeted'));
assert.isTrue(list.children[3].classList.contains('targeted'));
@@ -69,7 +67,7 @@
// We should still be at the end.
assert.equal(result, CursorMoveResult.CLIPPED);
assert.equal(cursor.index, 3);
- assert.equal(cursor.target, list.children[3]);
+ assert.equal(cursor.target, list.children[3] as HTMLElement);
assert.isTrue(cursor.isAtEnd());
// Wind the cursor all the way back to the first stop.
@@ -82,7 +80,7 @@
// The element state should reflect the start of the list.
assert.equal(cursor.index, 0);
- assert.equal(cursor.target, list.children[0]);
+ assert.equal(cursor.target, list.children[0] as HTMLElement);
assert.isTrue(cursor.isAtStart());
assert.isTrue(list.children[0].classList.contains('targeted'));
@@ -118,7 +116,7 @@
assert.equal(result, CursorMoveResult.MOVED);
assert.equal(cursor.index, 0);
- assert.equal(cursor.target, list.children[0]);
+ assert.equal(cursor.target, list.children[0] as HTMLElement);
assert.isTrue(list.children[0].classList.contains('targeted'));
assert.isTrue(cursor.isAtStart());
assert.isFalse(cursor.isAtEnd());
@@ -141,7 +139,7 @@
assert.equal(result, CursorMoveResult.MOVED);
const lastIndex = list.children.length - 1;
assert.equal(cursor.index, lastIndex);
- assert.equal(cursor.target, list.children[lastIndex]);
+ assert.equal(cursor.target, list.children[lastIndex] as HTMLElement);
assert.isTrue(list.children[lastIndex].classList.contains('targeted'));
assert.isFalse(cursor.isAtStart());
assert.isTrue(cursor.isAtEnd());
@@ -161,7 +159,7 @@
// Initialize the cursor with its stops.
cursor.stops = [...list.querySelectorAll('li')];
// Select the first stop.
- cursor.setCursor(list.children[0]);
+ cursor.setCursor(list.children[0] as HTMLElement);
const getTargetHeight = sinon.stub();
// Move the cursor without an optional get target height function.
@@ -176,7 +174,7 @@
test('_moveCursor from for invalid index does not check height', () => {
cursor.stops = [];
const getTargetHeight = sinon.stub();
- cursor._moveCursor(1, () => false, {getTargetHeight});
+ cursor._moveCursor(1, {filter: () => false, getTargetHeight});
assert.isFalse(getTargetHeight.called);
});
@@ -194,12 +192,12 @@
});
test('move with filter', () => {
- const isLetterB = function(row) {
+ const isLetterB = function (row: HTMLElement) {
return row.textContent === 'B';
};
cursor.stops = [...list.querySelectorAll('li')];
// Start cursor at the first stop.
- cursor.setCursor(list.children[0]);
+ cursor.setCursor(list.children[0] as HTMLElement);
// Move forward to meet the next condition.
cursor.next({filter: isLetterB});
@@ -225,19 +223,19 @@
test('focusOnMove prop', () => {
const listEls = [...list.querySelectorAll('li')];
- for (let i = 0; i < listEls.length; i++) {
- sinon.spy(listEls[i], 'focus');
- }
+ const listFocusStubs = listEls.map(listEl => sinon.spy(listEl, 'focus'));
cursor.stops = listEls;
- cursor.setCursor(list.children[0]);
+ cursor.setCursor(list.children[0] as HTMLElement);
cursor.focusOnMove = false;
cursor.next();
- assert.isFalse(cursor.target.focus.called);
+ assert.equal(listEls[1], cursor.target);
+ assert.isFalse(listFocusStubs[1].called);
cursor.focusOnMove = true;
cursor.next();
- assert.isTrue(cursor.target.focus.called);
+ assert.equal(listEls[2], cursor.target);
+ assert.isTrue(listFocusStubs[2].called);
});
suite('circular options', () => {
@@ -247,26 +245,26 @@
});
test('previous() on first element goes to last element', () => {
- cursor.setCursor(list.children[0]);
+ cursor.setCursor(list.children[0] as HTMLElement);
cursor.previous(options);
assert.equal(cursor.index, list.children.length - 1);
});
test('next() on last element goes to first element', () => {
- cursor.setCursor(list.children[list.children.length - 1]);
+ cursor.setCursor(list.children[list.children.length - 1] as HTMLElement);
cursor.next(options);
assert.equal(cursor.index, 0);
});
});
suite('_scrollToTarget', () => {
- let scrollStub;
+ let scrollStub: sinon.SinonStub;
setup(() => {
cursor.stops = [...list.querySelectorAll('li')];
cursor.scrollMode = 'keep-visible';
// There is a target which has a targetNext
- cursor.setCursor(list.children[0]);
+ cursor.setCursor(list.children[0] as HTMLElement);
cursor._moveCursor(1);
scrollStub = sinon.stub(window, 'scrollTo');
window.innerHeight = 60;
@@ -285,8 +283,9 @@
});
test('Called when top is visible, bottom is not, scroll is lower', () => {
- const visibleStub = sinon.stub(cursor, '_targetIsVisible').callsFake(
- () => visibleStub.callCount === 2);
+ const visibleStub = sinon
+ .stub(cursor, '_targetIsVisible')
+ .callsFake(() => visibleStub.callCount === 2);
window.scrollX = 123;
window.scrollY = 15;
window.innerHeight = 1000;
@@ -299,8 +298,9 @@
});
test('Called when top is visible, bottom not, scroll is higher', () => {
- const visibleStub = sinon.stub(cursor, '_targetIsVisible').callsFake(
- () => visibleStub.callCount === 2);
+ const visibleStub = sinon
+ .stub(cursor, '_targetIsVisible')
+ .callsFake(() => visibleStub.callCount === 2);
window.scrollX = 123;
window.scrollY = 25;
window.innerHeight = 1000;
@@ -316,8 +316,8 @@
window.scrollY = 25;
window.innerHeight = 300;
window.pageYOffset = 0;
- assert.equal(cursor._calculateScrollToValue(1000, {offsetHeight: 10}),
- 905);
+ const fakeElement = {offsetHeight: 10} as HTMLElement;
+ assert.equal(cursor._calculateScrollToValue(1000, fakeElement), 905);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 15d7072..019bec1 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -8,7 +8,6 @@
import '../gr-button/gr-button';
import '../gr-select/gr-select';
import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
-import {getAppContext} from '../../../services/app-context';
import {subscribe} from '../../lit/subscription-controller';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -18,6 +17,8 @@
import {fire} from '../../../utils/event-util';
import {ValueChangedEvent} from '../../../types/events';
import {GrSelect} from '../gr-select/gr-select';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-diff-preferences')
export class GrDiffPreferences extends LitElement {
@@ -51,13 +52,13 @@
@state() private originalDiffPrefs?: DiffPreferencesInfo;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
this.originalDiffPrefs = diffPreferences;
@@ -314,7 +315,7 @@
async save() {
if (!this.diffPrefs) return;
- await this.userModel.updateDiffPreference(this.diffPrefs);
+ await this.getUserModel().updateDiffPreference(this.diffPrefs);
fire(this, 'has-unsaved-changes-changed', {
value: this.hasUnsavedChanges(),
});
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 2d93227..886894e 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -16,6 +16,8 @@
import {customElement, property, state} from 'lit/decorators.js';
import {fire} from '../../../utils/event-util';
import {BindValueChangeEvent} from '../../../types/events';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
declare global {
interface HTMLElementEventMap {
@@ -53,7 +55,7 @@
private readonly restApiService = getAppContext().restApiService;
// Private but used in tests.
- readonly userModel = getAppContext().userModel;
+ readonly getUserModel = resolve(this, userModelToken);
private subscriptions: Subscription[] = [];
@@ -63,7 +65,7 @@
this.loggedIn = loggedIn;
});
this.subscriptions.push(
- this.userModel.preferences$.subscribe(prefs => {
+ this.getUserModel().preferences$.subscribe(prefs => {
if (prefs?.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
this.selectedScheme = prefs.download_scheme.toLowerCase();
@@ -194,7 +196,7 @@
this.selectedScheme = scheme;
fire(this, 'selected-scheme-changed', {value: scheme});
if (this.loggedIn) {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
download_scheme: this.selectedScheme,
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 695c674..b1d4e36 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -18,6 +18,8 @@
import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
import {fixture, html, assert} from '@open-wc/testing';
import {PaperTabElement} from '@polymer/paper-tabs/paper-tab';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-download-commands', () => {
let element: GrDownloadCommands;
@@ -170,11 +172,16 @@
});
});
suite('authenticated', () => {
- test('loads scheme from preferences', async () => {
- const element: GrDownloadCommands = await fixture(
+ let element: GrDownloadCommands;
+ let userModel: UserModel;
+ setup(async () => {
+ userModel = testResolver(userModelToken);
+ element = await fixture(
html`<gr-download-commands></gr-download-commands>`
);
- element.userModel.setPreferences({
+ });
+ test('loads scheme from preferences', async () => {
+ userModel.setPreferences({
...createPreferences(),
download_scheme: 'repo',
});
@@ -182,10 +189,7 @@
});
test('normalize scheme from preferences', async () => {
- const element: GrDownloadCommands = await fixture(
- html`<gr-download-commands></gr-download-commands>`
- );
- element.userModel.setPreferences({
+ userModel.setPreferences({
...createPreferences(),
download_scheme: 'REPO',
});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index d6a4d94..e176598 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -26,6 +26,8 @@
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
import {fontStyles} from '../../../styles/gr-font-styles';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {resolve} from '../../../models/dependency';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -90,7 +92,7 @@
@state() newContent = '';
- private readonly storage = getAppContext().storageService;
+ private readonly getStorage = resolve(this, storageServiceToken);
private readonly reporting = getAppContext().reportingService;
@@ -321,14 +323,14 @@
this.storeTask,
() => {
if (this.newContent.length) {
- this.storage.setEditableContentItem(storageKey, this.newContent);
+ this.getStorage().setEditableContentItem(storageKey, this.newContent);
} else {
// This does not really happen, because we don't clear newContent
// after saving (see below). So this only occurs when the user clears
// all the content in the editable textarea. But GrStorage cleans
// up itself after one day, so we are not so concerned about leaving
// some garbage behind.
- this.storage.eraseEditableContentItem(storageKey);
+ this.getStorage().eraseEditableContentItem(storageKey);
}
},
STORAGE_DEBOUNCE_INTERVAL_MS
@@ -358,7 +360,7 @@
let content;
if (this.storageKey) {
- const storedContent = this.storage.getEditableContentItem(
+ const storedContent = this.getStorage().getEditableContentItem(
this.storageKey
);
if (storedContent?.message) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index fec347c6..b4f25ce 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -6,17 +6,22 @@
import '../../../test/common-test-setup';
import './gr-editable-content';
import {GrEditableContent} from './gr-editable-content';
-import {query, queryAndAssert, stubStorage} from '../../../test/test-utils';
+import {query, queryAndAssert} from '../../../test/test-utils';
import {GrButton} from '../gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';
+import {StorageService} from '../../../services/storage/gr-storage';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-editable-content tests', () => {
let element: GrEditableContent;
+ let storageService: StorageService;
setup(async () => {
element = await fixture(html`<gr-editable-content></gr-editable-content>`);
await element.updateComplete;
+ storageService = testResolver(storageServiceToken);
});
test('renders', () => {
@@ -177,7 +182,7 @@
});
test('editing toggled to true, has stored data', async () => {
- stubStorage('getEditableContentItem').returns({
+ sinon.stub(storageService, 'getEditableContentItem').returns({
message: 'stored content',
updated: 0,
});
@@ -189,7 +194,7 @@
});
test('editing toggled to true, has no stored data', async () => {
- stubStorage('getEditableContentItem').returns(null);
+ sinon.stub(storageService, 'getEditableContentItem').returns(null);
element.editing = true;
await element.updateComplete;
@@ -199,8 +204,8 @@
});
test('edits are cached', async () => {
- const storeStub = stubStorage('setEditableContentItem');
- const eraseStub = stubStorage('eraseEditableContentItem');
+ const storeStub = sinon.stub(storageService, 'setEditableContentItem');
+ const eraseStub = sinon.stub(storageService, 'eraseEditableContentItem');
element.editing = true;
// Needed because editingChanged resets newContent
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts
index 578eda4..943f4de 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts
@@ -10,7 +10,7 @@
import '../gr-tooltip-content/gr-tooltip-content';
import '../gr-icon/gr-icon';
-function statusString(status: FileInfoStatus) {
+function statusString(status?: FileInfoStatus) {
if (!status) return '';
switch (status) {
case FileInfoStatus.ADDED:
@@ -115,15 +115,17 @@
private renderStatus() {
const classes = ['status', this.status];
- return html`<gr-tooltip-content title=${this.computeLabel()} has-tooltip>
- <div
- class=${classes.join(' ')}
- tabindex="0"
- aria-label=${this.computeLabel()}
+ return html`
+ <gr-tooltip-content
+ title=${this.computeLabel()}
+ has-tooltip
+ aria-label=${statusString(this.status)}
>
- ${this.renderIconOrLetter()}
- </div>
- </gr-tooltip-content>`;
+ <div class=${classes.join(' ')} aria-hidden="true">
+ ${this.renderIconOrLetter()}
+ </div>
+ </gr-tooltip-content>
+ `;
}
private renderIconOrLetter() {
@@ -135,13 +137,15 @@
private renderNewlyChanged() {
if (!this.newlyChanged) return;
- return html`<gr-tooltip-content title=${this.computeLabel()} has-tooltip>
- <gr-icon
- icon="new_releases"
- class="size-16"
- aria-label=${this.computeLabel()}
- ></gr-icon>
- </gr-tooltip-content>`;
+ return html`
+ <gr-tooltip-content
+ title=${this.computeLabel()}
+ has-tooltip
+ aria-label="newly"
+ >
+ <gr-icon icon="new_releases" class="size-16"></gr-icon>
+ </gr-tooltip-content>
+ `;
}
private computeLabel() {
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts
index 3bf877e..555b237 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts
@@ -28,8 +28,8 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-tooltip-content has-tooltip="" title="">
- <div class="status" aria-label="" tabindex="0"><span></span></div>
+ <gr-tooltip-content has-tooltip="" title="" aria-label="">
+ <div class="status" aria-hidden="true"><span></span></div>
</gr-tooltip-content>
`
);
@@ -40,8 +40,8 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-tooltip-content has-tooltip="" title="Added">
- <div class="A status" aria-label="Added" tabindex="0">
+ <gr-tooltip-content has-tooltip="" title="Added" aria-label="Added">
+ <div class="A status" aria-hidden="true">
<span>A</span>
</div>
</gr-tooltip-content>
@@ -54,15 +54,19 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-tooltip-content has-tooltip="" title="Newly Added">
- <gr-icon
- icon="new_releases"
- class="size-16"
- aria-label="Newly Added"
- ></gr-icon>
+ <gr-tooltip-content
+ has-tooltip=""
+ title="Newly Added"
+ aria-label="newly"
+ >
+ <gr-icon icon="new_releases" class="size-16"></gr-icon>
</gr-tooltip-content>
- <gr-tooltip-content has-tooltip="" title="Newly Added">
- <div class="A status" aria-label="Newly Added" tabindex="0">
+ <gr-tooltip-content
+ has-tooltip=""
+ title="Newly Added"
+ aria-label="Added"
+ >
+ <div class="A status" aria-hidden="true">
<span>A</span>
</div>
</gr-tooltip-content>
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 5a1db30..0cfd24c 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
@@ -84,11 +84,6 @@
:not(pre) > code {
display: inline;
}
- p {
- /* prose will automatically wrap but inline <code> blocks won't and we
- should overflow in that case rather than wrapping or leaking out */
- overflow-x: auto;
- }
li {
margin-left: var(--spacing-xl);
}
@@ -100,6 +95,11 @@
white-space: var(--linked-text-white-space, pre-wrap);
word-wrap: var(--linked-text-word-wrap, break-word);
}
+ .markdown-html {
+ /* prose will automatically wrap but inline <code> blocks won't and we
+ should overflow in that case rather than wrapping or leaking out */
+ overflow-x: auto;
+ }
`,
];
@@ -181,7 +181,7 @@
.callback=${(_error: string | null, contents: string) =>
sanitizeHtml(contents)}
>
- <div slot="markdown-html"></div>
+ <div class="markdown-html" slot="markdown-html"></div>
</marked-element>
`;
}
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 ca498318..218c04e 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
@@ -225,7 +225,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>text</p>
<p>
text with plain link:
@@ -279,7 +279,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<h1>h1-heading</h1>
<h2>h2-heading</h2>
<h3>h3-heading</h3>
@@ -323,7 +323,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>
<code>inline code</code>
</p>
@@ -353,7 +353,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<pre>
<code>multiline code</code>
</pre>
@@ -380,7 +380,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>![img](google.com/img.png)</p>
</div>
</marked-element>
@@ -399,7 +399,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>
@
<a
@@ -427,7 +427,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>
<gr-account-chip></gr-account-chip>
</p>
@@ -456,7 +456,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>
<code>@</code>
<a
@@ -481,7 +481,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>
<a href="https://www.google.com" rel="noopener" target="_blank"
>myLink</a
@@ -504,7 +504,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<blockquote>
<p>block quote</p>
</blockquote>
@@ -552,7 +552,7 @@
element,
/* HTML */ `
<marked-element>
- <div slot="markdown-html">
+ <div slot="markdown-html" class="markdown-html">
<p>plain text ${escapedDiv}</p>
<p>
<code>inline code ${escapedDiv}</code>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
new file mode 100644
index 0000000..dc08648
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -0,0 +1,566 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../gr-avatar/gr-avatar';
+import '../gr-button/gr-button';
+import '../gr-icon/gr-icon';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import {getAppContext} from '../../../services/app-context';
+import {
+ accountKey,
+ computeVoteableText,
+ isAccountEmailOnly,
+ isSelf,
+} from '../../../utils/account-util';
+import {customElement, property, state} from 'lit/decorators.js';
+import {
+ AccountInfo,
+ ChangeInfo,
+ ServerInfo,
+ ReviewInput,
+} from '../../../types/common';
+import {
+ canHaveAttention,
+ getAddedByReason,
+ getLastUpdate,
+ getReason,
+ getRemovedByReason,
+ hasAttention,
+} from '../../../utils/attention-set-util';
+import {ReviewerState} from '../../../constants/constants';
+import {CURRENT} from '../../../utils/patch-set-util';
+import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {css, html, LitElement, nothing} from 'lit';
+import {ifDefined} from 'lit/directives/if-defined.js';
+import {EventType} from '../../../types/events';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
+import {createSearchUrl} from '../../../models/views/search';
+import {createDashboardUrl} from '../../../models/views/dashboard';
+import {fire, fireEvent} from '../../../utils/event-util';
+import {userModelToken} from '../../../models/user/user-model';
+
+@customElement('gr-hovercard-account-contents')
+export class GrHovercardAccountContents extends LitElement {
+ @property({type: Object})
+ account!: AccountInfo;
+
+ @state()
+ selfAccount?: AccountInfo;
+
+ /**
+ * Optional ChangeInfo object, typically comes from the change page or
+ * from a row in a list of search results. This is needed for some change
+ * related features like adding the user as a reviewer.
+ */
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ /**
+ * Should attention set related features be shown in the component? Note
+ * that the information whether the user is in the attention set or not is
+ * part of the ChangeInfo object in the change property.
+ */
+ @property({type: Boolean})
+ highlightAttention = false;
+
+ @state()
+ serverConfig?: ServerInfo;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getUserModel().account$,
+ x => (this.selfAccount = x)
+ );
+ subscribe(
+ this,
+ () => this.getConfigModel().serverConfig$,
+ config => {
+ this.serverConfig = config;
+ }
+ );
+ }
+
+ static override get styles() {
+ return [
+ sharedStyles,
+ fontStyles,
+ css`
+ .top,
+ .attention,
+ .status,
+ .voteable {
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .links {
+ padding: var(--spacing-m) 0px var(--spacing-l) var(--spacing-xxl);
+ }
+ .top {
+ display: flex;
+ padding-top: var(--spacing-xl);
+ min-width: 300px;
+ }
+ gr-avatar {
+ height: 48px;
+ width: 48px;
+ margin-right: var(--spacing-l);
+ }
+ .title,
+ .email {
+ color: var(--deemphasized-text-color);
+ }
+ .action {
+ border-top: 1px solid var(--border-color);
+ padding: var(--spacing-s) var(--spacing-l);
+ --gr-button-padding: var(--spacing-s) var(--spacing-m);
+ }
+ .attention {
+ background-color: var(--emphasis-color);
+ }
+ .attention a {
+ text-decoration: none;
+ }
+ .status gr-icon {
+ font-size: 14px;
+ position: relative;
+ top: 2px;
+ }
+ gr-icon.attentionIcon {
+ transform: scaleX(0.8);
+ }
+ gr-icon.linkIcon {
+ font-size: var(--line-height-normal, 20px);
+ color: var(--deemphasized-text-color);
+ padding-right: 12px;
+ }
+ .links a {
+ color: var(--link-color);
+ padding: 0px 4px;
+ }
+ .reason {
+ padding-top: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar .account=${this.account} .imageSize=${56}></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="name heading-3">${this.account.name}</h3>
+ <div class="email">${this.account.email}</div>
+ </div>
+ </div>
+ ${this.renderAccountStatusPlugins()} ${this.renderAccountStatus()}
+ ${this.renderLinks()} ${this.renderChangeRelatedInfoAndActions()}
+ `;
+ }
+
+ private renderChangeRelatedInfoAndActions() {
+ if (this.change === undefined) {
+ return nothing;
+ }
+ const voteableText = computeVoteableText(this.change, this.account);
+ return html`
+ ${voteableText
+ ? html`
+ <div class="voteable">
+ <span class="title">Voteable:</span>
+ <span class="value">${voteableText}</span>
+ </div>
+ `
+ : ''}
+ ${this.renderNeedsAttention()} ${this.renderAddToAttention()}
+ ${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
+ `;
+ }
+
+ private renderReviewerOrCcActions() {
+ // `selfAccount` is required so that logged out users can't perform actions.
+ if (!this.selfAccount || !isRemovableReviewer(this.change, this.account))
+ return nothing;
+ return html`
+ <div class="action">
+ <gr-button
+ class="removeReviewerOrCC"
+ link
+ no-uppercase
+ @click=${this.handleRemoveReviewerOrCC}
+ >
+ Remove ${this.computeReviewerOrCCText()}
+ </gr-button>
+ </div>
+ <div class="action">
+ <gr-button
+ class="changeReviewerOrCC"
+ link
+ no-uppercase
+ @click=${this.handleChangeReviewerOrCCStatus}
+ >
+ ${this.computeChangeReviewerOrCCText()}
+ </gr-button>
+ </div>
+ `;
+ }
+
+ private renderAccountStatusPlugins() {
+ return html`
+ <gr-endpoint-decorator name="hovercard-status">
+ <gr-endpoint-param
+ name="account"
+ .value=${this.account}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `;
+ }
+
+ private renderLinks() {
+ if (!this.account || isAccountEmailOnly(this.account)) return nothing;
+ return html` <div class="links">
+ <gr-icon icon="link" class="linkIcon"></gr-icon>
+ <a
+ href=${ifDefined(this.computeOwnerChangesLink())}
+ @click=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ @enter=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ >
+ Changes
+ </a>
+ ·
+ <a
+ href=${ifDefined(this.computeOwnerDashboardLink())}
+ @click=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ @enter=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ >
+ Dashboard
+ </a>
+ </div>`;
+ }
+
+ private renderAccountStatus() {
+ if (!this.account.status) return nothing;
+ return html`
+ <div class="status">
+ <span class="title">About me:</span>
+ <span class="value">${this.account.status}</span>
+ </div>
+ `;
+ }
+
+ private renderNeedsAttention() {
+ if (!(this.isAttentionEnabled && this.hasUserAttention)) return nothing;
+ const lastUpdate = getLastUpdate(this.account, this.change);
+ return html`
+ <div class="attention">
+ <div>
+ <gr-icon
+ icon="label_important"
+ filled
+ small
+ class="attentionIcon"
+ ></gr-icon>
+ <span> ${this.computePronoun()} turn to take action. </span>
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
+ target="_blank"
+ >
+ <gr-icon icon="help" title="read documentation"></gr-icon>
+ </a>
+ </div>
+ <div class="reason">
+ <span class="title">Reason:</span>
+ <span class="value">
+ ${getReason(this.serverConfig, this.account, this.change)}
+ </span>
+ ${lastUpdate
+ ? html` (
+ <gr-date-formatter
+ withTooltip
+ .dateStr=${lastUpdate}
+ ></gr-date-formatter>
+ )`
+ : ''}
+ </div>
+ </div>
+ `;
+ }
+
+ private renderAddToAttention() {
+ if (!this.computeShowActionAddToAttentionSet()) return nothing;
+ return html`
+ <div class="action">
+ <gr-button
+ class="addToAttentionSet"
+ link
+ no-uppercase
+ @click=${this.handleClickAddToAttentionSet}
+ >
+ Add to attention set
+ </gr-button>
+ </div>
+ `;
+ }
+
+ private renderRemoveFromAttention() {
+ if (!this.computeShowActionRemoveFromAttentionSet()) return nothing;
+ return html`
+ <div class="action">
+ <gr-button
+ class="removeFromAttentionSet"
+ link
+ no-uppercase
+ @click=${this.handleClickRemoveFromAttentionSet}
+ >
+ Remove from attention set
+ </gr-button>
+ </div>
+ `;
+ }
+
+ // private but used by tests
+ computePronoun() {
+ if (!this.account || !this.selfAccount) return '';
+ return isSelf(this.account, this.selfAccount) ? 'Your' : 'Their';
+ }
+
+ computeOwnerChangesLink() {
+ if (!this.account) return undefined;
+ return createSearchUrl({
+ owner:
+ this.account.email ||
+ this.account.username ||
+ this.account.name ||
+ `${this.account._account_id}`,
+ });
+ }
+
+ computeOwnerDashboardLink() {
+ if (!this.account) return undefined;
+ if (this.account._account_id)
+ return createDashboardUrl({user: `${this.account._account_id}`});
+ if (this.account.email)
+ return createDashboardUrl({user: this.account.email});
+ return undefined;
+ }
+
+ get isAttentionEnabled() {
+ return (
+ !!this.highlightAttention &&
+ !!this.change &&
+ canHaveAttention(this.account)
+ );
+ }
+
+ get hasUserAttention() {
+ return hasAttention(this.account, this.change);
+ }
+
+ private getReviewerState(change: ChangeInfo) {
+ if (
+ change.reviewers[ReviewerState.REVIEWER]?.some(
+ (reviewer: AccountInfo) =>
+ reviewer._account_id === this.account._account_id
+ )
+ ) {
+ return ReviewerState.REVIEWER;
+ }
+ return ReviewerState.CC;
+ }
+
+ private computeReviewerOrCCText() {
+ if (!this.change || !this.account) return '';
+ return this.getReviewerState(this.change) === ReviewerState.REVIEWER
+ ? 'Reviewer'
+ : 'CC';
+ }
+
+ private computeChangeReviewerOrCCText() {
+ if (!this.change || !this.account) return '';
+ return this.getReviewerState(this.change) === ReviewerState.REVIEWER
+ ? 'Move Reviewer to CC'
+ : 'Move CC to Reviewer';
+ }
+
+ private handleChangeReviewerOrCCStatus() {
+ assertIsDefined(this.change, 'change');
+ // accountKey() throws an error if _account_id & email is not found, which
+ // we want to check before showing reloading toast
+ const _accountKey = accountKey(this.account);
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Reloading page...',
+ });
+ const reviewInput: Partial<ReviewInput> = {};
+ reviewInput.reviewers = [
+ {
+ reviewer: _accountKey,
+ state:
+ this.getReviewerState(this.change) === ReviewerState.CC
+ ? ReviewerState.REVIEWER
+ : ReviewerState.CC,
+ },
+ ];
+
+ this.restApiService
+ .saveChangeReview(this.change._number, CURRENT, reviewInput)
+ .then(response => {
+ if (!response || !response.ok) {
+ throw new Error(
+ 'something went wrong when toggling' +
+ this.getReviewerState(this.change!)
+ );
+ }
+ fire(this, 'reload', {clearPatchset: true});
+ });
+ }
+
+ private handleRemoveReviewerOrCC() {
+ if (!this.change || !(this.account?._account_id || this.account?.email))
+ throw new Error('Missing change or account.');
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Reloading page...',
+ });
+ this.restApiService
+ .removeChangeReviewer(
+ this.change._number,
+ (this.account?._account_id || this.account?.email)!
+ )
+ .then((response: Response | undefined) => {
+ if (!response || !response.ok) {
+ throw new Error('something went wrong when removing user');
+ }
+ fire(this, 'reload', {clearPatchset: true});
+ return response;
+ });
+ }
+
+ private computeShowActionAddToAttentionSet() {
+ const involvedOrSelf =
+ isInvolved(this.change, this.selfAccount) ||
+ isSelf(this.account, this.selfAccount);
+ return involvedOrSelf && this.isAttentionEnabled && !this.hasUserAttention;
+ }
+
+ private computeShowActionRemoveFromAttentionSet() {
+ const involvedOrSelf =
+ isInvolved(this.change, this.selfAccount) ||
+ isSelf(this.account, this.selfAccount);
+ return involvedOrSelf && this.isAttentionEnabled && this.hasUserAttention;
+ }
+
+ private handleClickAddToAttentionSet() {
+ if (!this.change || !this.account._account_id) return;
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Reloading page...',
+ dismissOnNavigation: true,
+ });
+
+ // We are deliberately updating the UI before making the API call. It is a
+ // risk that we are taking to achieve a better UX for 99.9% of the cases.
+ const reason = getAddedByReason(this.selfAccount, this.serverConfig);
+
+ if (!this.change.attention_set) this.change.attention_set = {};
+ this.change.attention_set[this.account._account_id] = {
+ account: this.account,
+ reason,
+ reason_account: this.selfAccount,
+ };
+ fireEvent(this, 'attention-set-updated');
+
+ this.reporting.reportInteraction(
+ 'attention-hovercard-add',
+ this.reportingDetails()
+ );
+ this.restApiService
+ .addToAttentionSet(this.change._number, this.account._account_id, reason)
+ .then(() => {
+ fireEvent(this, 'hide-alert');
+ });
+ fireEvent(this, 'action-taken');
+ }
+
+ private handleClickRemoveFromAttentionSet() {
+ if (!this.change || !this.account._account_id) return;
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Saving attention set update ...',
+ dismissOnNavigation: true,
+ });
+
+ // We are deliberately updating the UI before making the API call. It is a
+ // risk that we are taking to achieve a better UX for 99.9% of the cases.
+
+ const reason = getRemovedByReason(this.selfAccount, this.serverConfig);
+ if (this.change.attention_set)
+ delete this.change.attention_set[this.account._account_id];
+ fireEvent(this, 'attention-set-updated');
+
+ this.reporting.reportInteraction(
+ 'attention-hovercard-remove',
+ this.reportingDetails()
+ );
+ this.restApiService
+ .removeFromAttentionSet(
+ this.change._number,
+ this.account._account_id,
+ reason
+ )
+ .then(() => {
+ fireEvent(this, 'hide-alert');
+ });
+ fireEvent(this, 'action-taken');
+ }
+
+ private reportingDetails() {
+ const targetId = this.account._account_id;
+ const ownerId =
+ (this.change && this.change.owner && this.change.owner._account_id) || -1;
+ const selfId = (this.selfAccount && this.selfAccount._account_id) || -1;
+ const reviewers =
+ this.change && this.change.reviewers && this.change.reviewers.REVIEWER
+ ? [...this.change.reviewers.REVIEWER]
+ : [];
+ const reviewerIds = reviewers
+ .map(r => r._account_id)
+ .filter(rId => rId !== ownerId);
+ return {
+ actionByOwner: selfId === ownerId,
+ actionByReviewer: selfId !== -1 && reviewerIds.includes(selfId),
+ targetIsOwner: targetId === ownerId,
+ targetIsReviewer: reviewerIds.includes(targetId),
+ targetIsSelf: targetId === selfId,
+ };
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-hovercard-account-contents': GrHovercardAccountContents;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
new file mode 100644
index 0000000..b217562
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -0,0 +1,386 @@
+/**
+ * @license
+ * Copyright 2020 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {fixture, assert} from '@open-wc/testing';
+import {html} from 'lit';
+import './gr-hovercard-account-contents';
+import {GrHovercardAccountContents} from './gr-hovercard-account-contents';
+import {
+ mockPromise,
+ query,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {
+ AccountDetailInfo,
+ AccountId,
+ EmailAddress,
+ ReviewerState,
+} from '../../../api/rest-api';
+import {
+ createAccountDetailWithId,
+ createChange,
+ createDetailedLabelInfo,
+} from '../../../test/test-data-generators';
+import {GrButton} from '../gr-button/gr-button';
+import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken} from '../../../models/user/user-model';
+
+suite('gr-hovercard-account-contents tests', () => {
+ let element: GrHovercardAccountContents;
+
+ const ACCOUNT: AccountDetailInfo = {
+ ...createAccountDetailWithId(31),
+ email: 'kermit@gmail.com' as EmailAddress,
+ username: 'kermit',
+ name: 'Kermit The Frog',
+ status: 'I am a frog',
+ _account_id: 31415926535 as AccountId,
+ };
+
+ setup(async () => {
+ const change = {
+ ...createChange(),
+ attention_set: {},
+ reviewers: {},
+ owner: {...ACCOUNT},
+ };
+ element = await fixture(
+ html`<gr-hovercard-account-contents .account=${ACCOUNT} .change=${change}>
+ </gr-hovercard-account-contents>`
+ );
+ testResolver(userModelToken).setAccount({...ACCOUNT});
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar hidden=""></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="heading-3 name">Kermit The Frog</h3>
+ <div class="email">kermit@gmail.com</div>
+ </div>
+ </div>
+ <gr-endpoint-decorator name="hovercard-status">
+ <gr-endpoint-param name="account"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <div class="status">
+ <span class="title">About me:</span>
+ <span class="value">I am a frog</span>
+ </div>
+ <div class="links">
+ <gr-icon icon="link" class="linkIcon"></gr-icon>
+ <a href="/q/owner:kermit%2540gmail.com">Changes</a>
+ ·
+ <a href="/dashboard/31415926535">Dashboard</a>
+ </div>
+ `
+ );
+ });
+
+ test('renders without change data', async () => {
+ const elementWithoutChange = await fixture(
+ html`<gr-hovercard-account-contents
+ .account=${ACCOUNT}
+ ></gr-hovercard-account-contents>`
+ );
+ assert.shadowDom.equal(
+ elementWithoutChange,
+ /* HTML */ `
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar hidden=""></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="heading-3 name">Kermit The Frog</h3>
+ <div class="email">kermit@gmail.com</div>
+ </div>
+ </div>
+ <gr-endpoint-decorator name="hovercard-status">
+ <gr-endpoint-param name="account"> </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <div class="status">
+ <span class="title"> About me: </span>
+ <span class="value"> I am a frog </span>
+ </div>
+ <div class="links">
+ <gr-icon class="linkIcon" icon="link"> </gr-icon>
+ <a href="/q/owner:kermit%2540gmail.com"> Changes </a>
+ ·
+ <a href="/dashboard/31415926535"> Dashboard </a>
+ </div>
+ `
+ );
+ });
+
+ test('account name is shown', () => {
+ const name = queryAndAssert<HTMLHeadingElement>(element, '.name');
+ assert.equal(name.innerText, 'Kermit The Frog');
+ });
+
+ test('computePronoun', async () => {
+ element.account = createAccountDetailWithId(1);
+ element.selfAccount = createAccountDetailWithId(1);
+ await element.updateComplete;
+ assert.equal(element.computePronoun(), 'Your');
+ element.account = createAccountDetailWithId(2);
+ await element.updateComplete;
+ assert.equal(element.computePronoun(), 'Their');
+ });
+
+ test('account status is not shown if the property is not set', async () => {
+ element.account = {...ACCOUNT, status: undefined};
+ await element.updateComplete;
+ assert.isUndefined(query(element, '.status'));
+ });
+
+ test('account status is displayed', () => {
+ const status = queryAndAssert<HTMLSpanElement>(element, '.status .value');
+ assert.equal(status.innerText, 'I am a frog');
+ });
+
+ test('voteable div is not shown if the property is not set', () => {
+ assert.isUndefined(query(element, '.voteable'));
+ });
+
+ test('voteable div is displayed', async () => {
+ element.change = {
+ ...createChange(),
+ labels: {
+ Foo: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ _account_id: 7 as AccountId,
+ permitted_voting_range: {max: 2, min: 0},
+ },
+ ],
+ },
+ Bar: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createAccountDetailWithId(1),
+ permitted_voting_range: {max: 1, min: 0},
+ },
+ {
+ _account_id: 7 as AccountId,
+ permitted_voting_range: {max: 1, min: 0},
+ },
+ ],
+ },
+ FooBar: {
+ ...createDetailedLabelInfo(),
+ all: [{_account_id: 7 as AccountId, value: 0}],
+ },
+ },
+ permitted_labels: {
+ Foo: ['-1', ' 0', '+1', '+2'],
+ FooBar: ['-1', ' 0'],
+ },
+ };
+ element.account = createAccountDetailWithId(1);
+
+ await element.updateComplete;
+ const voteableEl = queryAndAssert<HTMLSpanElement>(
+ element,
+ '.voteable .value'
+ );
+ assert.equal(voteableEl.innerText, 'Bar: +1');
+ });
+
+ test('remove reviewer', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [ACCOUNT],
+ },
+ };
+ await element.updateComplete;
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+ const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
+ assert.isOk(button);
+ assert.equal(button.innerText, 'Remove Reviewer');
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('move reviewer to cc', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [ACCOUNT],
+ },
+ };
+ await element.updateComplete;
+ const saveReviewStub = stubRestApi('saveChangeReview').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+
+ const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
+
+ assert.isOk(button);
+ assert.equal(button.innerText, 'Move Reviewer to CC');
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(saveReviewStub.called);
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('move reviewer to cc', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [],
+ },
+ };
+ await element.updateComplete;
+ const saveReviewStub = stubRestApi('saveChangeReview').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+
+ const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
+ assert.isOk(button);
+ assert.equal(button.innerText, 'Move CC to Reviewer');
+
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(saveReviewStub.called);
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('remove cc', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [],
+ },
+ };
+ await element.updateComplete;
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+
+ const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
+
+ assert.equal(button.innerText, 'Remove CC');
+ assert.isOk(button);
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('add to attention set', async () => {
+ const apiPromise = mockPromise<Response>();
+ const apiSpy = stubRestApi('addToAttentionSet').returns(apiPromise);
+ element.highlightAttention = true;
+ await element.updateComplete;
+ const showAlertListener = sinon.spy();
+ const hideAlertListener = sinon.spy();
+ const updatedListener = sinon.spy();
+ element.addEventListener(EventType.SHOW_ALERT, showAlertListener);
+ element.addEventListener('hide-alert', hideAlertListener);
+ element.addEventListener('attention-set-updated', updatedListener);
+
+ const button = queryAndAssert<GrButton>(element, '.addToAttentionSet');
+ assert.isOk(button);
+ button.click();
+
+ assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 1);
+ const attention_set_info = Object.values(
+ element.change?.attention_set ?? {}
+ )[0];
+ assert.equal(
+ attention_set_info.reason,
+ `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
+ ' using the hovercard menu'
+ );
+ assert.equal(
+ attention_set_info.reason_account?._account_id,
+ ACCOUNT._account_id
+ );
+ assert.isTrue(showAlertListener.called, 'showAlertListener was called');
+ assert.isTrue(updatedListener.called, 'updatedListener was called');
+
+ apiPromise.resolve({...new Response(), ok: true});
+ await element.updateComplete;
+ assert.isTrue(apiSpy.calledOnce);
+ assert.equal(
+ apiSpy.lastCall.args[2],
+ `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
+ ' using the hovercard menu'
+ );
+ assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+ });
+
+ test('remove from attention set', async () => {
+ const apiPromise = mockPromise<Response>();
+ const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
+ element.highlightAttention = true;
+ element.change = {
+ ...createChange(),
+ attention_set: {
+ '31415926535': {account: ACCOUNT, reason: 'a good reason'},
+ },
+ reviewers: {},
+ owner: {...ACCOUNT},
+ };
+ await element.updateComplete;
+ const showAlertListener = sinon.spy();
+ const hideAlertListener = sinon.spy();
+ const updatedListener = sinon.spy();
+ element.addEventListener(EventType.SHOW_ALERT, showAlertListener);
+ element.addEventListener('hide-alert', hideAlertListener);
+ element.addEventListener('attention-set-updated', updatedListener);
+
+ const button = queryAndAssert<GrButton>(element, '.removeFromAttentionSet');
+ assert.isOk(button);
+ button.click();
+
+ assert.isDefined(element.change?.attention_set);
+ assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 0);
+ assert.isTrue(showAlertListener.called, 'showAlertListener was called');
+ assert.isTrue(updatedListener.called, 'updatedListener was called');
+
+ apiPromise.resolve({...new Response(), ok: true});
+ await element.updateComplete;
+
+ assert.isTrue(apiSpy.calledOnce);
+ assert.equal(
+ apiSpy.lastCall.args[2],
+ `Removed by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
+ ' using the hovercard menu'
+ );
+ assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 9647141..543f5bc 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -8,42 +8,12 @@
import '../gr-icon/gr-icon';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
-import {getAppContext} from '../../../services/app-context';
-import {
- accountKey,
- computeVoteableText,
- isAccountEmailOnly,
- isSelf,
-} from '../../../utils/account-util';
-import {customElement, property, state} from 'lit/decorators.js';
-import {
- AccountInfo,
- ChangeInfo,
- ServerInfo,
- ReviewInput,
-} from '../../../types/common';
-import {
- canHaveAttention,
- getAddedByReason,
- getLastUpdate,
- getReason,
- getRemovedByReason,
- hasAttention,
-} from '../../../utils/attention-set-util';
-import {ReviewerState} from '../../../constants/constants';
-import {CURRENT} from '../../../utils/patch-set-util';
-import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
-import {assertIsDefined} from '../../../utils/common-util';
-import {fontStyles} from '../../../styles/gr-font-styles';
-import {css, html, LitElement, nothing} from 'lit';
-import {ifDefined} from 'lit/directives/if-defined.js';
+import {customElement, property} from 'lit/decorators.js';
+import {AccountInfo, ChangeInfo} from '../../../types/common';
+import {html, LitElement} from 'lit';
import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
-import {EventType} from '../../../types/events';
-import {subscribe} from '../../lit/subscription-controller';
-import {resolve} from '../../../models/dependency';
-import {configModelToken} from '../../../models/config/config-model';
-import {createSearchUrl} from '../../../models/views/search';
-import {createDashboardUrl} from '../../../models/views/dashboard';
+import {when} from 'lit/directives/when.js';
+import './gr-hovercard-account-contents';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
const base = HovercardMixin(LitElement);
@@ -53,9 +23,6 @@
@property({type: Object})
account!: AccountInfo;
- @state()
- selfAccount?: AccountInfo;
-
/**
* Optional ChangeInfo object, typically comes from the change page or
* from a row in a list of search results. This is needed for some change
@@ -72,498 +39,30 @@
@property({type: Boolean})
highlightAttention = false;
- @state()
- serverConfig?: ServerInfo;
-
- private readonly restApiService = getAppContext().restApiService;
-
- private readonly reporting = getAppContext().reportingService;
-
- // private but used in tests
- readonly userModel = getAppContext().userModel;
-
- private readonly getConfigModel = resolve(this, configModelToken);
-
- constructor() {
- super();
- subscribe(
- this,
- () => this.userModel.account$,
- x => (this.selfAccount = x)
- );
- subscribe(
- this,
- () => this.getConfigModel().serverConfig$,
- config => {
- this.serverConfig = config;
- }
- );
- }
-
- static override get styles() {
- return [
- fontStyles,
- base.styles || [],
- css`
- .top,
- .attention,
- .status,
- .voteable {
- padding: var(--spacing-s) var(--spacing-l);
- }
- .links {
- padding: var(--spacing-m) 0px var(--spacing-l) var(--spacing-xxl);
- }
- .top {
- display: flex;
- padding-top: var(--spacing-xl);
- min-width: 300px;
- }
- gr-avatar {
- height: 48px;
- width: 48px;
- margin-right: var(--spacing-l);
- }
- .title,
- .email {
- color: var(--deemphasized-text-color);
- }
- .action {
- border-top: 1px solid var(--border-color);
- padding: var(--spacing-s) var(--spacing-l);
- --gr-button-padding: var(--spacing-s) var(--spacing-m);
- }
- .attention {
- background-color: var(--emphasis-color);
- }
- .attention a {
- text-decoration: none;
- }
- .status gr-icon {
- font-size: 14px;
- position: relative;
- top: 2px;
- }
- gr-icon.attentionIcon {
- transform: scaleX(0.8);
- }
- gr-icon.linkIcon {
- font-size: var(--line-height-normal, 20px);
- color: var(--deemphasized-text-color);
- padding-right: 12px;
- }
- .links a {
- color: var(--link-color);
- padding: 0px 4px;
- }
- .reason {
- padding-top: var(--spacing-s);
- }
- `,
- ];
- }
-
override render() {
return html`
<div id="container" role="tooltip" tabindex="-1">
- ${this.renderContent()}
+ ${when(
+ this._isShowing,
+ () =>
+ html`<gr-hovercard-account-contents
+ .account=${this.account}
+ .change=${this.change}
+ .highlightAttention=${this.highlightAttention}
+ @link-clicked=${this.forceHide}
+ @action-taken=${this.mouseHide}
+ @attention-set-updated=${this.redirectEventToTarget}
+ @hide-alert=${this.redirectEventToTarget}
+ @show-alert=${this.redirectEventToTarget}
+ @reload=${this.redirectEventToTarget}
+ ></gr-hovercard-account-contents>`
+ )}
</div>
`;
}
- private renderContent() {
- if (!this._isShowing) return;
- return html`
- <div class="top">
- <div class="avatar">
- <gr-avatar .account=${this.account} imageSize="56"></gr-avatar>
- </div>
- <div class="account">
- <h3 class="name heading-3">${this.account.name}</h3>
- <div class="email">${this.account.email}</div>
- </div>
- </div>
- ${this.renderAccountStatusPlugins()} ${this.renderAccountStatus()}
- ${this.renderLinks()} ${this.renderChangeRelatedInfoAndActions()}
- `;
- }
-
- private renderChangeRelatedInfoAndActions() {
- if (this.change === undefined) {
- return;
- }
- const voteableText = computeVoteableText(this.change, this.account);
- return html`
- ${voteableText
- ? html`
- <div class="voteable">
- <span class="title">Voteable:</span>
- <span class="value">${voteableText}</span>
- </div>
- `
- : ''}
- ${this.renderNeedsAttention()} ${this.renderAddToAttention()}
- ${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
- `;
- }
-
- private renderReviewerOrCcActions() {
- if (!this.selfAccount || !isRemovableReviewer(this.change, this.account))
- return;
- return html`
- <div class="action">
- <gr-button
- class="removeReviewerOrCC"
- link=""
- no-uppercase
- @click=${this.handleRemoveReviewerOrCC}
- >
- Remove ${this.computeReviewerOrCCText()}
- </gr-button>
- </div>
- <div class="action">
- <gr-button
- class="changeReviewerOrCC"
- link=""
- no-uppercase
- @click=${this.handleChangeReviewerOrCCStatus}
- >
- ${this.computeChangeReviewerOrCCText()}
- </gr-button>
- </div>
- `;
- }
-
- private renderAccountStatusPlugins() {
- return html`
- <gr-endpoint-decorator name="hovercard-status">
- <gr-endpoint-param
- name="account"
- .value=${this.account}
- ></gr-endpoint-param>
- </gr-endpoint-decorator>
- `;
- }
-
- private renderLinks() {
- if (!this.account || isAccountEmailOnly(this.account)) return nothing;
- return html` <div class="links">
- <gr-icon icon="link" class="linkIcon"></gr-icon
- ><a
- href=${ifDefined(this.computeOwnerChangesLink())}
- @click=${() => {
- this.forceHide();
- return true;
- }}
- @enter=${() => {
- this.forceHide();
- return true;
- }}
- >Changes</a
- >·<a
- href=${ifDefined(this.computeOwnerDashboardLink())}
- @click=${() => {
- this.forceHide();
- return true;
- }}
- @enter=${() => {
- this.forceHide();
- return true;
- }}
- >Dashboard</a
- >
- </div>`;
- }
-
- private renderAccountStatus() {
- if (!this.account.status) return;
- return html`
- <div class="status">
- <span class="title">About me:</span>
- <span class="value">${this.account.status}</span>
- </div>
- `;
- }
-
- private renderNeedsAttention() {
- if (!(this.isAttentionEnabled && this.hasUserAttention)) return;
- const lastUpdate = getLastUpdate(this.account, this.change);
- return html`
- <div class="attention">
- <div>
- <gr-icon
- icon="label_important"
- filled
- small
- class="attentionIcon"
- ></gr-icon>
- <span> ${this.computePronoun()} turn to take action. </span>
- <a
- href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
- target="_blank"
- >
- <gr-icon icon="help" title="read documentation"></gr-icon>
- </a>
- </div>
- <div class="reason">
- <span class="title">Reason:</span>
- <span class="value">
- ${getReason(this.serverConfig, this.account, this.change)}
- </span>
- ${lastUpdate
- ? html` (<gr-date-formatter
- withTooltip
- .dateStr=${lastUpdate}
- ></gr-date-formatter
- >)`
- : ''}
- </div>
- </div>
- `;
- }
-
- private renderAddToAttention() {
- if (!this.computeShowActionAddToAttentionSet()) return;
- return html`
- <div class="action">
- <gr-button
- class="addToAttentionSet"
- link=""
- no-uppercase
- @click=${this.handleClickAddToAttentionSet}
- >
- Add to attention set
- </gr-button>
- </div>
- `;
- }
-
- private renderRemoveFromAttention() {
- if (!this.computeShowActionRemoveFromAttentionSet()) return;
- return html`
- <div class="action">
- <gr-button
- class="removeFromAttentionSet"
- link=""
- no-uppercase
- @click=${this.handleClickRemoveFromAttentionSet}
- >
- Remove from attention set
- </gr-button>
- </div>
- `;
- }
-
- // private but used by tests
- computePronoun() {
- if (!this.account || !this.selfAccount) return '';
- return isSelf(this.account, this.selfAccount) ? 'Your' : 'Their';
- }
-
- computeOwnerChangesLink() {
- if (!this.account) return undefined;
- return createSearchUrl({
- owner:
- this.account.email ||
- this.account.username ||
- this.account.name ||
- `${this.account._account_id}`,
- });
- }
-
- computeOwnerDashboardLink() {
- if (!this.account) return undefined;
- if (this.account._account_id)
- return createDashboardUrl({user: `${this.account._account_id}`});
- if (this.account.email)
- return createDashboardUrl({user: this.account.email});
- return undefined;
- }
-
- get isAttentionEnabled() {
- return (
- !!this.highlightAttention &&
- !!this.change &&
- canHaveAttention(this.account)
- );
- }
-
- get hasUserAttention() {
- return hasAttention(this.account, this.change);
- }
-
- private getReviewerState() {
- if (
- this.change!.reviewers[ReviewerState.REVIEWER]?.some(
- (reviewer: AccountInfo) =>
- reviewer._account_id === this.account._account_id
- )
- ) {
- return ReviewerState.REVIEWER;
- }
- return ReviewerState.CC;
- }
-
- private computeReviewerOrCCText() {
- if (!this.change || !this.account) return '';
- return this.getReviewerState() === ReviewerState.REVIEWER
- ? 'Reviewer'
- : 'CC';
- }
-
- private computeChangeReviewerOrCCText() {
- if (!this.change || !this.account) return '';
- return this.getReviewerState() === ReviewerState.REVIEWER
- ? 'Move Reviewer to CC'
- : 'Move CC to Reviewer';
- }
-
- private handleChangeReviewerOrCCStatus() {
- assertIsDefined(this.change, 'change');
- // accountKey() throws an error if _account_id & email is not found, which
- // we want to check before showing reloading toast
- const _accountKey = accountKey(this.account);
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Reloading page...',
- });
- const reviewInput: Partial<ReviewInput> = {};
- reviewInput.reviewers = [
- {
- reviewer: _accountKey,
- state:
- this.getReviewerState() === ReviewerState.CC
- ? ReviewerState.REVIEWER
- : ReviewerState.CC,
- },
- ];
-
- this.restApiService
- .saveChangeReview(this.change._number, CURRENT, reviewInput)
- .then(response => {
- if (!response || !response.ok) {
- throw new Error(
- 'something went wrong when toggling' + this.getReviewerState()
- );
- }
- this.dispatchEventThroughTarget('reload', {clearPatchset: true});
- });
- }
-
- private handleRemoveReviewerOrCC() {
- if (!this.change || !(this.account?._account_id || this.account?.email))
- throw new Error('Missing change or account.');
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Reloading page...',
- });
- this.restApiService
- .removeChangeReviewer(
- this.change._number,
- (this.account?._account_id || this.account?.email)!
- )
- .then((response: Response | undefined) => {
- if (!response || !response.ok) {
- throw new Error('something went wrong when removing user');
- }
- this.dispatchEventThroughTarget('reload', {clearPatchset: true});
- return response;
- });
- }
-
- private computeShowActionAddToAttentionSet() {
- const involvedOrSelf =
- isInvolved(this.change, this.selfAccount) ||
- isSelf(this.account, this.selfAccount);
- return involvedOrSelf && this.isAttentionEnabled && !this.hasUserAttention;
- }
-
- private computeShowActionRemoveFromAttentionSet() {
- const involvedOrSelf =
- isInvolved(this.change, this.selfAccount) ||
- isSelf(this.account, this.selfAccount);
- return involvedOrSelf && this.isAttentionEnabled && this.hasUserAttention;
- }
-
- private handleClickAddToAttentionSet(e: MouseEvent) {
- if (!this.change || !this.account._account_id) return;
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Saving attention set update ...',
- dismissOnNavigation: true,
- });
-
- // We are deliberately updating the UI before making the API call. It is a
- // risk that we are taking to achieve a better UX for 99.9% of the cases.
- const reason = getAddedByReason(this.selfAccount, this.serverConfig);
-
- if (!this.change.attention_set) this.change.attention_set = {};
- this.change.attention_set[this.account._account_id] = {
- account: this.account,
- reason,
- reason_account: this.selfAccount,
- };
- this.dispatchEventThroughTarget('attention-set-updated');
-
- this.reporting.reportInteraction(
- 'attention-hovercard-add',
- this.reportingDetails()
- );
- this.restApiService
- .addToAttentionSet(this.change._number, this.account._account_id, reason)
- .then(() => {
- this.dispatchEventThroughTarget('hide-alert');
- });
- this.mouseHide(e);
- }
-
- private handleClickRemoveFromAttentionSet(e: MouseEvent) {
- if (!this.change || !this.account._account_id) return;
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Saving attention set update ...',
- dismissOnNavigation: true,
- });
-
- // We are deliberately updating the UI before making the API call. It is a
- // risk that we are taking to achieve a better UX for 99.9% of the cases.
-
- const reason = getRemovedByReason(this.selfAccount, this.serverConfig);
- if (this.change.attention_set)
- delete this.change.attention_set[this.account._account_id];
- this.dispatchEventThroughTarget('attention-set-updated');
-
- this.reporting.reportInteraction(
- 'attention-hovercard-remove',
- this.reportingDetails()
- );
- this.restApiService
- .removeFromAttentionSet(
- this.change._number,
- this.account._account_id,
- reason
- )
- .then(() => {
- this.dispatchEventThroughTarget('hide-alert');
- });
- this.mouseHide(e);
- }
-
- private reportingDetails() {
- const targetId = this.account._account_id;
- const ownerId =
- (this.change && this.change.owner && this.change.owner._account_id) || -1;
- const selfId = (this.selfAccount && this.selfAccount._account_id) || -1;
- const reviewers =
- this.change && this.change.reviewers && this.change.reviewers.REVIEWER
- ? [...this.change.reviewers.REVIEWER]
- : [];
- const reviewerIds = reviewers
- .map(r => r._account_id)
- .filter(rId => rId !== ownerId);
- return {
- actionByOwner: selfId === ownerId,
- actionByReviewer: selfId !== -1 && reviewerIds.includes(selfId),
- targetIsOwner: targetId === ownerId,
- targetIsReviewer: reviewerIds.includes(targetId),
- targetIsSelf: targetId === selfId,
- };
+ private redirectEventToTarget(e: CustomEvent<unknown>) {
+ this.dispatchEventThroughTarget(e.type, e.detail);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
index 89bc043..40e4c75 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
@@ -8,56 +8,35 @@
import {html} from 'lit';
import './gr-hovercard-account';
import {GrHovercardAccount} from './gr-hovercard-account';
-import {
- mockPromise,
- query,
- queryAndAssert,
- stubRestApi,
-} from '../../../test/test-utils';
-import {
- AccountDetailInfo,
- AccountId,
- EmailAddress,
- ReviewerState,
-} from '../../../api/rest-api';
+import {queryAndAssert} from '../../../test/test-utils';
import {
createAccountDetailWithId,
createChange,
- createDetailedLabelInfo,
} from '../../../test/test-data-generators';
import {GrButton} from '../gr-button/gr-button';
-import {EventType} from '../../../types/events';
+import {GrHovercardAccountContents} from './gr-hovercard-account-contents';
+import {userModelToken} from '../../../models/user/user-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-hovercard-account tests', () => {
let element: GrHovercardAccount;
-
- const ACCOUNT: AccountDetailInfo = {
- ...createAccountDetailWithId(31),
- email: 'kermit@gmail.com' as EmailAddress,
- username: 'kermit',
- name: 'Kermit The Frog',
- status: 'I am a frog',
- _account_id: 31415926535 as AccountId,
- };
+ let contents: GrHovercardAccountContents;
setup(async () => {
- const change = {
- ...createChange(),
- attention_set: {},
- reviewers: {},
- owner: {...ACCOUNT},
- };
+ const account = createAccountDetailWithId(31);
element = await fixture<GrHovercardAccount>(
html`<gr-hovercard-account
class="hovered"
- .account=${ACCOUNT}
- .change=${change}
+ .account=${account}
+ .change=${createChange()}
+ .highlightAttention=${true}
>
</gr-hovercard-account>`
);
await element.show({});
- element.userModel.setAccount({...ACCOUNT});
+ testResolver(userModelToken).setAccount({...account});
await element.updateComplete;
+ contents = queryAndAssert(element, 'gr-hovercard-account-contents');
});
teardown(async () => {
@@ -70,337 +49,29 @@
element,
/* HTML */ `
<div id="container" role="tooltip" tabindex="-1">
- <div class="top">
- <div class="avatar">
- <gr-avatar hidden="" imagesize="56"></gr-avatar>
- </div>
- <div class="account">
- <h3 class="heading-3 name">Kermit The Frog</h3>
- <div class="email">kermit@gmail.com</div>
- </div>
- </div>
- <gr-endpoint-decorator name="hovercard-status">
- <gr-endpoint-param name="account"></gr-endpoint-param>
- </gr-endpoint-decorator>
- <div class="status">
- <span class="title">About me:</span>
- <span class="value">I am a frog</span>
- </div>
- <div class="links">
- <gr-icon icon="link" class="linkIcon"></gr-icon>
- <a href="/q/owner:kermit%2540gmail.com">Changes</a>
- ·
- <a href="/dashboard/31415926535">Dashboard</a>
- </div>
+ <gr-hovercard-account-contents></gr-hovercard-account-contents>
</div>
`
);
});
- test('renders without change data', async () => {
- const elementWithoutChange = await fixture<GrHovercardAccount>(
- html`<gr-hovercard-account class="hovered" .account=${ACCOUNT}>
- </gr-hovercard-account>`
- );
- await elementWithoutChange.show({});
- assert.shadowDom.equal(
- elementWithoutChange,
- /* HTML */ `
- <div id="container" role="tooltip" tabindex="-1">
- <div class="top">
- <div class="avatar">
- <gr-avatar hidden="" imagesize="56"> </gr-avatar>
- </div>
- <div class="account">
- <h3 class="heading-3 name">Kermit The Frog</h3>
- <div class="email">kermit@gmail.com</div>
- </div>
- </div>
- <gr-endpoint-decorator name="hovercard-status">
- <gr-endpoint-param name="account"> </gr-endpoint-param>
- </gr-endpoint-decorator>
- <div class="status">
- <span class="title"> About me: </span>
- <span class="value"> I am a frog </span>
- </div>
- <div class="links">
- <gr-icon class="linkIcon" icon="link"> </gr-icon>
- <a href="/q/owner:kermit%2540gmail.com"> Changes </a>
- ·
- <a href="/dashboard/31415926535"> Dashboard </a>
- </div>
- </div>
- `
- );
- elementWithoutChange.mouseHide(new MouseEvent('click'));
- await elementWithoutChange.updateComplete;
+ test('hides when links are clicked', () => {
+ const changesLink = queryAndAssert<HTMLAnchorElement>(contents, 'a');
+ // Actually redirecting will break the test, replace URL with no-op
+ changesLink.href = 'javascript:';
+
+ assert.isTrue(element._isShowing);
+
+ changesLink.click();
+
+ assert.isFalse(element._isShowing);
});
- test('account name is shown', () => {
- const name = queryAndAssert<HTMLHeadingElement>(element, '.name');
- assert.equal(name.innerText, 'Kermit The Frog');
- });
+ test('hides when actions are performed', () => {
+ assert.isTrue(element._isShowing);
- test('computePronoun', async () => {
- element.account = createAccountDetailWithId(1);
- element.selfAccount = createAccountDetailWithId(1);
- await element.updateComplete;
- assert.equal(element.computePronoun(), 'Your');
- element.account = createAccountDetailWithId(2);
- await element.updateComplete;
- assert.equal(element.computePronoun(), 'Their');
- });
+ queryAndAssert<GrButton>(contents, 'gr-button.addToAttentionSet').click();
- test('account status is not shown if the property is not set', async () => {
- element.account = {...ACCOUNT, status: undefined};
- await element.updateComplete;
- assert.isUndefined(query(element, '.status'));
- });
-
- test('account status is displayed', () => {
- const status = queryAndAssert<HTMLSpanElement>(element, '.status .value');
- assert.equal(status.innerText, 'I am a frog');
- });
-
- test('voteable div is not shown if the property is not set', () => {
- assert.isUndefined(query(element, '.voteable'));
- });
-
- test('voteable div is displayed', async () => {
- element.change = {
- ...createChange(),
- labels: {
- Foo: {
- ...createDetailedLabelInfo(),
- all: [
- {
- _account_id: 7 as AccountId,
- permitted_voting_range: {max: 2, min: 0},
- },
- ],
- },
- Bar: {
- ...createDetailedLabelInfo(),
- all: [
- {
- ...createAccountDetailWithId(1),
- permitted_voting_range: {max: 1, min: 0},
- },
- {
- _account_id: 7 as AccountId,
- permitted_voting_range: {max: 1, min: 0},
- },
- ],
- },
- FooBar: {
- ...createDetailedLabelInfo(),
- all: [{_account_id: 7 as AccountId, value: 0}],
- },
- },
- permitted_labels: {
- Foo: ['-1', ' 0', '+1', '+2'],
- FooBar: ['-1', ' 0'],
- },
- };
- element.account = createAccountDetailWithId(1);
-
- await element.updateComplete;
- const voteableEl = queryAndAssert<HTMLSpanElement>(
- element,
- '.voteable .value'
- );
- assert.equal(voteableEl.innerText, 'Bar: +1');
- });
-
- test('remove reviewer', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [ACCOUNT],
- },
- };
- await element.updateComplete;
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
- const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
- assert.isOk(button);
- assert.equal(button.innerText, 'Remove Reviewer');
- button.click();
- await element.updateComplete;
- assert.isTrue(reloadListener.called);
- });
-
- test('move reviewer to cc', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [ACCOUNT],
- },
- };
- await element.updateComplete;
- const saveReviewStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
-
- const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
-
- assert.isOk(button);
- assert.equal(button.innerText, 'Move Reviewer to CC');
- button.click();
- await element.updateComplete;
- assert.isTrue(saveReviewStub.called);
- assert.isTrue(reloadListener.called);
- });
-
- test('move reviewer to cc', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [],
- },
- };
- await element.updateComplete;
- const saveReviewStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
-
- const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
- assert.isOk(button);
- assert.equal(button.innerText, 'Move CC to Reviewer');
-
- button.click();
- await element.updateComplete;
- assert.isTrue(saveReviewStub.called);
- assert.isTrue(reloadListener.called);
- });
-
- test('remove cc', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [],
- },
- };
- await element.updateComplete;
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
-
- const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
-
- assert.equal(button.innerText, 'Remove CC');
- assert.isOk(button);
- button.click();
- await element.updateComplete;
- assert.isTrue(reloadListener.called);
- });
-
- test('add to attention set', async () => {
- const apiPromise = mockPromise<Response>();
- const apiSpy = stubRestApi('addToAttentionSet').returns(apiPromise);
- element.highlightAttention = true;
- element._target = document.createElement('div');
- await element.updateComplete;
- const showAlertListener = sinon.spy();
- const hideAlertListener = sinon.spy();
- const updatedListener = sinon.spy();
- element._target.addEventListener(EventType.SHOW_ALERT, showAlertListener);
- element._target.addEventListener('hide-alert', hideAlertListener);
- element._target.addEventListener('attention-set-updated', updatedListener);
-
- const button = queryAndAssert<GrButton>(element, '.addToAttentionSet');
- assert.isOk(button);
- assert.isTrue(element._isShowing, 'hovercard is showing');
- button.click();
-
- assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 1);
- const attention_set_info = Object.values(
- element.change?.attention_set ?? {}
- )[0];
- assert.equal(
- attention_set_info.reason,
- `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
- ' using the hovercard menu'
- );
- assert.equal(
- attention_set_info.reason_account?._account_id,
- ACCOUNT._account_id
- );
- assert.isTrue(showAlertListener.called, 'showAlertListener was called');
- assert.isTrue(updatedListener.called, 'updatedListener was called');
- assert.isFalse(element._isShowing, 'hovercard is hidden');
-
- apiPromise.resolve({...new Response(), ok: true});
- await element.updateComplete;
- assert.isTrue(apiSpy.calledOnce);
- assert.equal(
- apiSpy.lastCall.args[2],
- `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
- ' using the hovercard menu'
- );
- assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
- });
-
- test('remove from attention set', async () => {
- const apiPromise = mockPromise<Response>();
- const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
- element.highlightAttention = true;
- element.change = {
- ...createChange(),
- attention_set: {
- '31415926535': {account: ACCOUNT, reason: 'a good reason'},
- },
- reviewers: {},
- owner: {...ACCOUNT},
- };
- element._target = document.createElement('div');
- await element.updateComplete;
- const showAlertListener = sinon.spy();
- const hideAlertListener = sinon.spy();
- const updatedListener = sinon.spy();
- element._target.addEventListener(EventType.SHOW_ALERT, showAlertListener);
- element._target.addEventListener('hide-alert', hideAlertListener);
- element._target.addEventListener('attention-set-updated', updatedListener);
-
- const button = queryAndAssert<GrButton>(element, '.removeFromAttentionSet');
- assert.isOk(button);
- assert.isTrue(element._isShowing, 'hovercard is showing');
- button.click();
-
- assert.isDefined(element.change?.attention_set);
- assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 0);
- assert.isTrue(showAlertListener.called, 'showAlertListener was called');
- assert.isTrue(updatedListener.called, 'updatedListener was called');
- assert.isFalse(element._isShowing, 'hovercard is hidden');
-
- apiPromise.resolve({...new Response(), ok: true});
- await element.updateComplete;
-
- assert.isTrue(apiSpy.calledOnce);
- assert.equal(
- apiSpy.lastCall.args[2],
- `Removed by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
- ' using the hovercard menu'
- );
- assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+ assert.isFalse(element._isShowing);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 65b1778..89c8770 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -10,165 +10,12 @@
$_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24">
<svg>
<defs>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=unfold_more -->
- <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"></path><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="more-horiz"><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
- <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
- <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="help"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/resources/icons/?icon=help_outline -->
- <g id="help-outline"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="info-outline"><path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=ic_hourglass_full-->
- <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=mode_comment-->
- <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=calendar_today-->
- <g id="calendar"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="lightbulb-outline"><path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="build"><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=check_circle-->
- <g id="check-circle"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=check_circle_outline-->
- <g id="check-circle-outline"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
- <!-- This SVG is a copy from https://fonts.google.com/icons?selected=Material+Icons:event_busy&icon.query=check+circle-->
- <g id="check-circle-filled"><path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M10,17l-4-4l1.4-1.4l2.6,2.6l6.6-6.6 L18,9L10,17z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
- <!-- This SVG is a copy from https://fonts.google.com/icons?selected=Material+Icons:event_busy&icon.query=block-->
- <g id="block"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="abandon"><path d="M17.65675,17.65725 C14.77275,20.54125 10.23775,20.75625 7.09875,18.31525 L18.31475,7.09925 C20.75575,10.23825 20.54075,14.77325 17.65675,17.65725 M6.34275,6.34325 C9.22675,3.45925 13.76275,3.24425 16.90075,5.68525 L5.68475,16.90125 C3.24375,13.76325 3.45875,9.22725 6.34275,6.34325 M19.07075,4.92925 C15.16575,1.02425 8.83375,1.02425 4.92875,4.92925 C1.02375,8.83425 1.02375,15.16625 4.92875,19.07125 C8.83375,22.97625 15.16575,22.97625 19.07075,19.07125 C22.97575,15.16625 22.97575,8.83425 19.07075,4.92925"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="edit"><path d="M3,17.2525 L3,21.0025 L6.75,21.0025 L17.81,9.9425 L14.06,6.1925 L3,17.2525 L3,17.2525 Z M20.71,7.0425 C21.1,6.6525 21.1,6.0225 20.71,5.6325 L18.37,3.2925 C17.98,2.9025 17.35,2.9025 16.96,3.2925 L15.13,5.1225 L18.88,8.8725 L20.71,7.0425 L20.71,7.0425 Z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="rebase"><path d="M15.5759,19.4241 L14.5861,20.4146 L11.7574,23.2426 L10.3434,21.8286 L12.171569,20 L7.82933006,20 C7.41754308,21.1652555 6.30635522,22 5,22 C3.343,22 2,20.657 2,19 C2,17.6936448 2.83474451,16.5824569 4,16.1706699 L4,7.82933006 C2.83474451,7.41754308 2,6.30635522 2,5 C2,3.343 3.343,2 5,2 C6.30635522,2 7.41754308,2.83474451 7.82933006,4 L12.1715,4 L10.3431,2.1716 L11.7571,0.7576 L15.36365,4.3633 L16.0000001,4.99920039 C16.0004321,3.34256796 17.3432665,2 19,2 C20.657,2 22,3.343 22,5 C22,6.30635522 21.1652555,7.41754308 20,7.82933006 L20,16.1706699 C21.1652555,16.5824569 22,17.6936448 22,19 C22,20.657 20.657,22 19,22 C17.343,22 16,20.657 16,19 L15.5759,19.4241 Z M12.1715,18 L10.3431,16.1716 L11.7571,14.7576 L15.36365,18.3633 L16.0000001,18.9992004 C16.0003407,17.6931914 16.8349823,16.5823729 18,16.1706699 L18,7.82933006 C16.8347445,7.41754308 16,6.30635522 16,5 L15.5759,5.4241 L14.5861,6.4146 L11.7574,9.2426 L10.3434,7.8286 L12.171569,6 L7.82933006,6 C7.52807271,6.85248394 6.85248394,7.52807271 6,7.82933006 L6,16.1706699 C6.85248394,16.4719273 7.52807271,17.1475161 7.82933006,18 L12.1715,18 Z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="rebaseEdit"><path d="M15.5759,19.4241 L14.5861,20.4146 L11.7574,23.2426 L10.3434,21.8286 L12.171569,20 L7.82933006,20 C7.41754308,21.1652555 6.30635522,22 5,22 C3.343,22 2,20.657 2,19 C2,17.6936448 2.83474451,16.5824569 4,16.1706699 L4,7.82933006 C2.83474451,7.41754308 2,6.30635522 2,5 C2,3.343 3.343,2 5,2 C6.30635522,2 7.41754308,2.83474451 7.82933006,4 L12.1715,4 L10.3431,2.1716 L11.7571,0.7576 L15.36365,4.3633 L16.0000001,4.99920039 C16.0004321,3.34256796 17.3432665,2 19,2 C20.657,2 22,3.343 22,5 C22,6.30635522 21.1652555,7.41754308 20,7.82933006 L20,16.1706699 C21.1652555,16.5824569 22,17.6936448 22,19 C22,20.657 20.657,22 19,22 C17.343,22 16,20.657 16,19 L15.5759,19.4241 Z M12.1715,18 L10.3431,16.1716 L11.7571,14.7576 L15.36365,18.3633 L16.0000001,18.9992004 C16.0003407,17.6931914 16.8349823,16.5823729 18,16.1706699 L18,7.82933006 C16.8347445,7.41754308 16,6.30635522 16,5 L15.5759,5.4241 L14.5861,6.4146 L11.7574,9.2426 L10.3434,7.8286 L12.171569,6 L7.82933006,6 C7.52807271,6.85248394 6.85248394,7.52807271 6,7.82933006 L6,16.1706699 C6.85248394,16.4719273 7.52807271,17.1475161 7.82933006,18 L12.1715,18 Z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="restore"><path d="M12,8 L12,13 L16.28,15.54 L17,14.33 L13.5,12.25 L13.5,8 L12,8 Z M13,3 C8.03,3 4,7.03 4,12 L1,12 L4.89,15.89 L4.96,16.03 L9,12 L6,12 C6,8.13 9.13,5 13,5 C16.87,5 20,8.13 20,12 C20,15.87 16.87,19 13,19 C11.07,19 9.32,18.21 8.06,16.94 L6.64,18.36 C8.27,19.99 10.51,21 13,21 C17.97,21 22,16.97 22,12 C22,7.03 17.97,3 13,3 Z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="revert"><path d="M12.3,8.5 C9.64999995,8.5 7.24999995,9.49 5.39999995,11.1 L1.79999995,7.5 L1.79999995,16.5 L10.8,16.5 L7.17999995,12.88 C8.56999995,11.72 10.34,11 12.3,11 C15.84,11 18.85,13.31 19.9,16.5 L22.27,15.72 C20.88,11.53 16.95,8.5 12.3,8.5"></path></g>
- <g id="revert_submission"><path d="M12.3,8.5 C9.64999995,8.5 7.24999995,9.49 5.39999995,11.1 L1.79999995,7.5 L1.79999995,16.5 L10.8,16.5 L7.17999995,12.88 C8.56999995,11.72 10.34,11 12.3,11 C15.84,11 18.85,13.31 19.9,16.5 L22.27,15.72 C20.88,11.53 16.95,8.5 12.3,8.5"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="stopEdit"><path d="M4 4 20 4 20 20 4 20z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="submit"><path d="M22.23,5 L11.65,15.58 L7.47000001,11.41 L6.06000001,12.82 L11.65,18.41 L23.649,6.41 L22.23,5 Z M16.58,5 L10.239,11.34 L11.65,12.75 L17.989,6.41 L16.58,5 Z M0.400000006,12.82 L5.99000001,18.41 L7.40000001,17 L1.82000001,11.41 L0.400000006,12.82 Z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
- <!-- This is a custom PolyGerrit SVG -->
- <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
- <!-- This SVG is an adaptation of material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=label_important-->
- <g id="attention"><path d="M1 23 l13 0 c.67 0 1.27 -.33 1.63 -.84 l7.37 -10.16 l-7.37 -10.16 c-.36 -.51 -.96 -.84 -1.63 -.84 L1 1 L7 12 z"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=pets-->
- <g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=visibility-->
- <g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
- <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons -->
- <g id="schedule"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=bug_report-->
- <g id="bug"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.gstatic.com/s/i/googlematerialicons/move_item/v1/24px.svg -->
- <g id="move-item"><path d="M15,19H5V5h10v4h2V5c0-1.1-0.89-2-2-2H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h10c1.11,0,2-0.9,2-2v-4h-2V19z"/><polygon points="20.01,8.01 18.59,9.41 20.17,11 8,11 8,13 20.17,13 18.59,14.59 20.01,15.99 24,12"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=warning-->
- <g id="warning"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=timelapse-->
- <g id="timelapse"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=mark_chat_read-->
- <g id="markChatRead"><path d="M12,18l-6,0l-4,4V4c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2v7l-2,0V4H4v12l8,0V18z M23,14.34l-1.41-1.41l-4.24,4.24l-2.12-2.12 l-1.41,1.41L17.34,20L23,14.34z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=message-->
- <g id="message"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=launch-->
- <g id="launch"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=filter-->
- <g id="filter"><path d="M0,0h24 M24,24H0" fill="none"/><path d="M4.25,5.61C6.27,8.2,10,13,10,13v6c0,0.55,0.45,1,1,1h2c0.55,0,1-0.45,1-1v-6c0,0,3.72-4.8,5.74-7.39 C20.25,4.95,19.78,4,18.95,4H5.04C4.21,4,3.74,4.95,4.25,5.61z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=arrow_drop_down-->
- <g id="arrowDropDown"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 10l5 5 5-5z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=arrow_drop_up-->
- <g id="arrowDropUp"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 14l5-5 5 5z"/></g>
- <!-- This is just a placeholder, i.e. an empty icon that has the same size as a normal icon. -->
- <g id="placeholder"><path d="M0 0h24v24H0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=insert_photo-->
- <g id="insert-photo"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=download-->
- <g id="download"><path d="M0 0h24v24H0z" fill="none"/><path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=system_update-->
- <g id="system-update"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=swap_horiz-->
<g id="swapHoriz"><path d="M0 0h24v24H0z" fill="none"/><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=link-->
- <g id="link"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aplay_arrow-->
<g id="playArrow"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Apause-->
<g id="pause"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Acode-->
- <g id="code"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Afile_present-->
- <g id="file-present"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V7l-5-5zM6 20V4h8v4h4v12H6zm10-10v5c0 2.21-1.79 4-4 4s-4-1.79-4-4V8.5c0-1.47 1.26-2.64 2.76-2.49 1.3.13 2.24 1.32 2.24 2.63V15h-2V8.5c0-.28-.22-.5-.5-.5s-.5.22-.5.5V15c0 1.1.9 2 2 2s2-.9 2-2v-5h2z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aarrow_forward-->
- <g id="arrow-forward"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:feedback -->
- <g id="feedback"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=description -->
- <g id="description"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=settings_backup_restore and 0.65 scale and 4 translate https://fonts.google.com/icons?selected=Material+Icons&icon.query=done-->
- <g id="overridden"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M12 15 zM2 4v6h6V8H5.09C6.47 5.61 9.04 4 12 4c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8H2c0 5.52 4.48 10 10.01 10C17.53 22 22 17.52 22 12S17.53 2 12.01 2C8.73 2 5.83 3.58 4 6.01V4H2z"/><path xmlns="http://www.w3.org/2000/svg" d="M9.85 14.53 7.12 11.8l-.91.91L9.85 16.35 17.65 8.55l-.91-.91L9.85 14.53z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:event_busy -->
- <g id="unavailable"><path d="M0 0h24v24H0z" fill="none"/><path d="M9.31 17l2.44-2.44L14.19 17l1.06-1.06-2.44-2.44 2.44-2.44L14.19 10l-2.44 2.44L9.31 10l-1.06 1.06 2.44 2.44-2.44 2.44L9.31 17zM19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/></g>
- <!-- This SVG is a custom PolyGerrit SVG -->
- <g id="not-working-hours"><path d="M20.8,13.9c-0.6,0.1-1.3,0.2-2,0.2c-4.9,0-8.9-4-8.9-8.9c0-0.7,0.1-1.4,0.2-2c-4,0.9-6.9,4.5-6.9,8.7c0,4.9,4,8.9,8.9,8.9C16.3,20.8,19.9,17.9,20.8,13.9z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:pending_actions -->
- <g id="scheduled"><path d="M0 0h24v24H0z" fill="none"/><path d="M17.0 22.0Q14.925 22.0 13.4625 20.5375Q12.0 19.075 12.0 17.0Q12.0 14.925 13.4625 13.4625Q14.925 12.0 17.0 12.0Q19.075 12.0 20.5375 13.4625Q22.0 14.925 22.0 17.0Q22.0 19.075 20.5375 20.5375Q19.075 22.0 17.0 22.0ZM18.675 19.375 19.375 18.675 17.5 16.8V14.0H16.5V17.2ZM5.0 21.0Q4.175 21.0 3.5875 20.4125Q3.0 19.825 3.0 19.0V5.0Q3.0 4.175 3.5875 3.5875Q4.175 3.0 5.0 3.0H9.175Q9.5 2.125 10.2625 1.5625Q11.025 1.0 12.0 1.0Q12.975 1.0 13.7375 1.5625Q14.5 2.125 14.825 3.0H19.0Q19.825 3.0 20.4125 3.5875Q21.0 4.175 21.0 5.0V11.25Q20.55 10.925 20.05 10.7Q19.55 10.475 19.0 10.3V5.0Q19.0 5.0 19.0 5.0Q19.0 5.0 19.0 5.0H17.0V8.0H7.0V5.0H5.0Q5.0 5.0 5.0 5.0Q5.0 5.0 5.0 5.0V19.0Q5.0 19.0 5.0 19.0Q5.0 19.0 5.0 19.0H10.3Q10.475 19.55 10.7 20.05Q10.925 20.55 11.25 21.0ZM12.0 5.0Q12.425 5.0 12.7125 4.7125Q13.0 4.425 13.0 4.0Q13.0 3.575 12.7125 3.2875Q12.425 3.0 12.0 3.0Q11.575 3.0 11.2875 3.2875Q11.0 3.575 11.0 4.0Q11.0 4.425 11.2875 4.7125Q11.575 5.0 12.0 5.0Z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:new_releases -->
- <g id="new"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M23 12l-2.44-2.78.34-3.68-3.61-.82-1.89-3.18L12 3 8.6 1.54 6.71 4.72l-3.61.81.34 3.68L1 12l2.44 2.78-.34 3.69 3.61.82 1.89 3.18L12 21l3.4 1.46 1.89-3.18 3.61-.82-.34-3.68L23 12zm-4.51 2.11l.26 2.79-2.74.62-1.43 2.41L12 18.82l-2.58 1.11-1.43-2.41-2.74-.62.26-2.8L3.66 12l1.85-2.12-.26-2.78 2.74-.61 1.43-2.41L12 5.18l2.58-1.11 1.43 2.41 2.74.62-.26 2.79L20.34 12l-1.85 2.11zM11 15h2v2h-2zm0-8h2v6h-2z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:arrow_right_alt -->
- <g id="arrow-right"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M14 6l-1.41 1.41L16.17 11H4v2h12.17l-3.58 3.59L14 18l6-6z"/></g>
- <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:cancel -->
- <g id="cancel"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.ts
similarity index 67%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.ts
index f103b60..6e63b0d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.ts
@@ -6,37 +6,43 @@
import '../../../test/common-test-setup';
import '../../change/gr-change-actions/gr-change-actions';
import {assert} from '@open-wc/testing';
+import {PluginApi} from '../../../api/plugin';
+import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
+import {Side} from '../../../constants/constants';
+import {DiffLayerListener} from '../../../types/types';
suite('gr-annotation-actions-js-api tests', () => {
- let annotationActions;
-
- let plugin;
+ let annotationActions: GrAnnotationActionsInterface;
+ let plugin: PluginApi;
setup(() => {
- window.Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- annotationActions = plugin.annotationApi();
- });
-
- teardown(() => {
- annotationActions = null;
+ window.Gerrit.install(
+ p => {
+ plugin = p;
+ },
+ '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js'
+ );
+ // The object is exposed as type AnnotationPluginApi, but the tests need the
+ // implementation class to arrange test setups using `.createLayer()`.
+ annotationActions = plugin.annotationApi() as GrAnnotationActionsInterface;
});
test('add notifier', () => {
const path1 = '/dummy/path1';
const path2 = '/dummy/path2';
- const annotationLayer1 = annotationActions.createLayer(path1, 1);
- const annotationLayer2 = annotationActions.createLayer(path2, 1);
+ const annotationLayer1 = annotationActions.createLayer(path1);
+ const annotationLayer2 = annotationActions.createLayer(path2);
const layer1Spy = sinon.spy(annotationLayer1, 'notifyListeners');
const layer2Spy = sinon.spy(annotationLayer2, 'notifyListeners');
// Assert that no layers are invoked with a different path.
- annotationActions.notify('/dummy/path3', 0, 10, 'right');
+ annotationActions.notify('/dummy/path3', 0, 10, Side.RIGHT);
assert.isFalse(layer1Spy.called);
assert.isFalse(layer2Spy.called);
// Assert that only the 1st layer is invoked with path1.
- annotationActions.notify(path1, 0, 10, 'right');
+ annotationActions.notify(path1, 0, 10, Side.RIGHT);
assert.isTrue(layer1Spy.called);
assert.isFalse(layer2Spy.called);
@@ -45,18 +51,18 @@
layer2Spy.resetHistory();
// Assert that only the 2nd layer is invoked with path2.
- annotationActions.notify(path2, 0, 20, 'left');
+ annotationActions.notify(path2, 0, 20, Side.LEFT);
assert.isFalse(layer1Spy.called);
assert.isTrue(layer2Spy.called);
});
test('layer notify listeners', () => {
- const annotationLayer = annotationActions.createLayer('/dummy/path', 1);
+ const annotationLayer = annotationActions.createLayer('/dummy/path');
let listenerCalledTimes = 0;
const startRange = 10;
const endRange = 20;
- const side = 'right';
- const listener = (st, end, s) => {
+ const side = Side.RIGHT;
+ const listener: DiffLayerListener = (st, end, s) => {
listenerCalledTimes++;
assert.equal(st, startRange);
assert.equal(end, endRange);
@@ -78,4 +84,3 @@
assert.equal(listenerCalledTimes, 3);
});
});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
index b8bfd21..f0143a6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
@@ -15,6 +15,7 @@
RevisionActions,
} from '../../../api/change-actions';
import {PropertyDeclaration} from 'lit';
+import {JsApiService} from './gr-js-api-types';
export interface UIActionInfo extends RequireProperties<ActionInfo, 'label'> {
__key: string;
@@ -65,9 +66,11 @@
private readonly reporting = getAppContext().reportingService;
- private readonly jsApiService = getAppContext().jsApiService;
-
- constructor(public plugin: PluginApi, el?: GrChangeActionsElement) {
+ constructor(
+ public plugin: PluginApi,
+ private readonly jsApiService: JsApiService,
+ el?: GrChangeActionsElement
+ ) {
this.reporting.trackApi(this.plugin, 'actions', 'constructor');
this.setEl(el);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
index df9adc9..ae93977 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
@@ -5,13 +5,7 @@
*/
import '../../../test/common-test-setup';
import '../../change/gr-change-actions/gr-change-actions';
-import {
- query,
- queryAll,
- queryAndAssert,
- resetPlugins,
-} from '../../../test/test-utils';
-import {getPluginLoader} from './gr-plugin-loader';
+import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
import {GrChangeActions} from '../../change/gr-change-actions/gr-change-actions';
import {fixture, html, assert} from '@open-wc/testing';
import {PluginApi} from '../../../api/plugin';
@@ -24,6 +18,8 @@
import {ChangeViewChangeInfo} from '../../../types/common';
import {GrDropdown} from '../gr-dropdown/gr-dropdown';
import {GrIcon} from '../gr-icon/gr-icon';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from './gr-plugin-loader';
suite('gr-change-actions-js-api-interface tests', () => {
let element: GrChangeActions;
@@ -32,7 +28,6 @@
suite('early init', () => {
setup(async () => {
- resetPlugins();
window.Gerrit.install(
p => {
plugin = p;
@@ -41,17 +36,13 @@
'http://test.com/plugins/testplugin/static/test.js'
);
// Mimic all plugins loaded.
- getPluginLoader().loadPlugins([]);
+ testResolver(pluginLoaderToken).loadPlugins([]);
changeActions = plugin.changeActions();
element = await fixture<GrChangeActions>(html`
<gr-change-actions></gr-change-actions>
`);
});
- teardown(() => {
- resetPlugins();
- });
-
test('does not throw', () => {
assert.doesNotThrow(() => {
changeActions.add(ActionType.CHANGE, 'foo');
@@ -61,7 +52,6 @@
suite('normal init', () => {
setup(async () => {
- resetPlugins();
element = await fixture<GrChangeActions>(html`
<gr-change-actions></gr-change-actions>
`);
@@ -76,11 +66,7 @@
);
changeActions = plugin.changeActions();
// Mimic all plugins loaded.
- getPluginLoader().loadPlugins([]);
- });
-
- teardown(() => {
- resetPlugins();
+ testResolver(pluginLoaderToken).loadPlugins([]);
});
test('property existence', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.ts
index 1d47d37..65d2687 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.ts
@@ -6,15 +6,17 @@
import '../../../test/common-test-setup';
import '../../change/gr-reply-dialog/gr-reply-dialog';
import {stubElement} from '../../../test/test-utils';
-import {assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
import {PluginApi} from '../../../api/plugin';
import {ChangeReplyPluginApi} from '../../../api/change-reply';
+import {GrReplyDialog} from '../../change/gr-reply-dialog/gr-reply-dialog';
+import {html} from 'lit';
suite('gr-change-reply-js-api tests', () => {
let changeReply: ChangeReplyPluginApi;
let plugin: PluginApi;
- suite('early init', () => {
+ suite('init', () => {
setup(async () => {
window.Gerrit.install(
p => {
@@ -24,39 +26,13 @@
'http://test.com/plugins/testplugin/static/test.js'
);
changeReply = plugin.changeReply();
+ await fixture<GrReplyDialog>(html`<gr-reply-dialog></gr-reply-dialog>>`);
+ assert.ok(changeReply);
});
test('works', () => {
stubElement('gr-reply-dialog', 'getLabelValue').returns('+123');
- assert.equal(changeReply.getLabelValue('My-Label'), '+123');
-
- const setLabelValueStub = stubElement('gr-reply-dialog', 'setLabelValue');
- changeReply.setLabelValue('My-Label', '+1337');
- assert.isTrue(setLabelValueStub.calledWithExactly('My-Label', '+1337'));
-
- const setPluginMessageStub = stubElement(
- 'gr-reply-dialog',
- 'setPluginMessage'
- );
- changeReply.showMessage('foobar');
- assert.isTrue(setPluginMessageStub.calledWithExactly('foobar'));
- });
- });
-
- suite('normal init', () => {
- setup(async () => {
- window.Gerrit.install(
- p => {
- plugin = p;
- },
- '0.1',
- 'http://test.com/plugins/testplugin/static/test.js'
- );
- changeReply = plugin.changeReply();
- });
-
- test('works', () => {
- stubElement('gr-reply-dialog', 'getLabelValue').returns('+123');
+ assert.ok(changeReply);
assert.equal(changeReply.getLabelValue('My-Label'), '+123');
const setLabelValueStub = stubElement('gr-reply-dialog', 'setLabelValue');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
deleted file mode 100644
index 4900ed5..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-/**
- * @license
- * Copyright 2019 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/**
- * This defines the Gerrit instance. All methods directly attached to Gerrit
- * should be defined or linked here.
- */
-import {getPluginLoader, PluginOptionMap} from './gr-plugin-loader';
-import {send} from './gr-api-utils';
-import {getAppContext, AppContext} from '../../../services/app-context';
-import {PluginApi} from '../../../api/plugin';
-import {AuthService} from '../../../services/gr-auth/gr-auth';
-import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
-import {HttpMethod} from '../../../constants/constants';
-import {RequestPayload} from '../../../types/common';
-import {
- EventCallback,
- EventEmitterService,
-} from '../../../services/gr-event-interface/gr-event-interface';
-import {Gerrit} from '../../../api/gerrit';
-import {fontStyles} from '../../../styles/gr-font-styles';
-import {formStyles} from '../../../styles/gr-form-styles';
-import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
-import {spinnerStyles} from '../../../styles/gr-spinner-styles';
-import {subpageStyles} from '../../../styles/gr-subpage-styles';
-import {tableStyles} from '../../../styles/gr-table-styles';
-import {assertIsDefined} from '../../../utils/common-util';
-import {iconStyles} from '../../../styles/gr-icon-styles';
-
-/**
- * These are the methods and properties that are exposed explicitly in the
- * public global `Gerrit` interface. In reality JavaScript plugins do depend
- * on some of this "internal" stuff. But we want to convert plugins to
- * TypeScript one by one and while doing that remove those dependencies.
- */
-export interface GerritInternal extends EventEmitterService, Gerrit {
- css(rule: string): string;
- install(
- callback: (plugin: PluginApi) => void,
- opt_version?: string,
- src?: string
- ): void;
- getLoggedIn(): Promise<boolean>;
- get(url: string, callback?: (response: unknown) => void): void;
- post(
- url: string,
- payload?: RequestPayload,
- callback?: (response: unknown) => void
- ): void;
- put(
- url: string,
- payload?: RequestPayload,
- callback?: (response: unknown) => void
- ): void;
- delete(url: string, callback?: (response: unknown) => void): void;
- isPluginLoaded(pathOrUrl: string): boolean;
- awaitPluginsLoaded(): Promise<unknown>;
- _loadPlugins(plugins: string[], opts: PluginOptionMap): void;
- _arePluginsLoaded(): boolean;
- _isPluginEnabled(pathOrUrl: string): boolean;
- _isPluginLoaded(pathOrUrl: string): boolean;
- _customStyleSheet?: CSSStyleSheet;
-
- // exposed methods
- Auth: AuthService;
-}
-
-export function initGerritPluginApi(appContext: AppContext) {
- window.Gerrit = window.Gerrit ?? new GerritImpl(appContext);
-}
-
-export function _testOnly_getGerritInternalPluginApi(): GerritInternal {
- if (!window.Gerrit) throw new Error('initGerritPluginApi was not called');
- return window.Gerrit as GerritInternal;
-}
-
-export function deprecatedDelete(
- url: string,
- callback?: (response: Response) => void
-) {
- console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
- return getAppContext()
- .restApiService.send(HttpMethod.DELETE, url)
- .then(response => {
- if (response.status !== 204) {
- return response.text().then(text => {
- if (text) {
- return Promise.reject(new Error(text));
- } else {
- return Promise.reject(new Error(`${response.status}`));
- }
- });
- }
- if (callback) callback(response);
- return response;
- });
-}
-
-const fakeApi = {
- getPluginName: () => 'global',
-};
-
-/**
- * TODO(brohlfs): Reduce this step by step until it only contains install().
- */
-class GerritImpl implements GerritInternal {
- _customStyleSheet?: CSSStyleSheet;
-
- public readonly Auth: AuthService;
-
- private readonly reportingService: ReportingService;
-
- private readonly eventEmitter: EventEmitterService;
-
- private readonly restApiService: RestApiService;
-
- public readonly styles = {
- font: fontStyles,
- form: formStyles,
- icon: iconStyles,
- menuPage: menuPageStyles,
- spinner: spinnerStyles,
- subPage: subpageStyles,
- table: tableStyles,
- };
-
- constructor(appContext: AppContext) {
- this.Auth = appContext.authService;
- this.reportingService = appContext.reportingService;
- this.eventEmitter = appContext.eventEmitter;
- this.restApiService = appContext.restApiService;
- assertIsDefined(this.reportingService, 'reportingService');
- assertIsDefined(this.eventEmitter, 'eventEmitter');
- assertIsDefined(this.restApiService, 'restApiService');
- }
-
- finalize() {}
-
- /**
- * @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
- * the documentation how to replace it accordingly.
- */
- css(rulesStr: string) {
- this.reportingService.trackApi(fakeApi, 'global', 'css');
- console.warn(
- 'Gerrit.css(rulesStr) is deprecated!',
- 'Use plugin.styles().css(rulesStr)'
- );
- if (!this._customStyleSheet) {
- const styleEl = document.createElement('style');
- document.head.appendChild(styleEl);
- this._customStyleSheet = styleEl.sheet!;
- }
-
- const name = `__pg_js_api_class_${this._customStyleSheet.cssRules.length}`;
- this._customStyleSheet.insertRule('.' + name + '{' + rulesStr + '}', 0);
- return name;
- }
-
- install(
- callback: (plugin: PluginApi) => void,
- version?: string,
- src?: string
- ) {
- getPluginLoader().install(callback, version, src);
- }
-
- getLoggedIn() {
- this.reportingService.trackApi(fakeApi, 'global', 'getLoggedIn');
- console.warn(
- 'Gerrit.getLoggedIn() is deprecated! ' +
- 'Use plugin.restApi().getLoggedIn()'
- );
- return this.restApiService.getLoggedIn();
- }
-
- get(url: string, callback?: (response: unknown) => void) {
- this.reportingService.trackApi(fakeApi, 'global', 'get');
- console.warn('.get() is deprecated! Use plugin.restApi().get()');
- send(this.restApiService, HttpMethod.GET, url, callback);
- }
-
- post(
- url: string,
- payload?: RequestPayload,
- callback?: (response: unknown) => void
- ) {
- this.reportingService.trackApi(fakeApi, 'global', 'post');
- console.warn('.post() is deprecated! Use plugin.restApi().post()');
- send(this.restApiService, HttpMethod.POST, url, callback, payload);
- }
-
- put(
- url: string,
- payload?: RequestPayload,
- callback?: (response: unknown) => void
- ) {
- this.reportingService.trackApi(fakeApi, 'global', 'put');
- console.warn('.put() is deprecated! Use plugin.restApi().put()');
- send(this.restApiService, HttpMethod.PUT, url, callback, payload);
- }
-
- delete(url: string, callback?: (response: Response) => void) {
- this.reportingService.trackApi(fakeApi, 'global', 'delete');
- deprecatedDelete(url, callback);
- }
-
- awaitPluginsLoaded() {
- this.reportingService.trackApi(fakeApi, 'global', 'awaitPluginsLoaded');
- return getPluginLoader().awaitPluginsLoaded();
- }
-
- // TODO(taoalpha): consider removing these proxy methods
- // and using getPluginLoader() directly
- _loadPlugins(plugins: string[] = []) {
- this.reportingService.trackApi(fakeApi, 'global', '_loadPlugins');
- getPluginLoader().loadPlugins(plugins);
- }
-
- _arePluginsLoaded() {
- this.reportingService.trackApi(fakeApi, 'global', '_arePluginsLoaded');
- return getPluginLoader().arePluginsLoaded();
- }
-
- _isPluginEnabled(pathOrUrl: string) {
- this.reportingService.trackApi(fakeApi, 'global', '_isPluginEnabled');
- return getPluginLoader().isPluginEnabled(pathOrUrl);
- }
-
- isPluginLoaded(pathOrUrl: string) {
- return this._isPluginLoaded(pathOrUrl);
- }
-
- _isPluginLoaded(pathOrUrl: string) {
- this.reportingService.trackApi(fakeApi, 'global', '_isPluginLoaded');
- return getPluginLoader().isPluginLoaded(pathOrUrl);
- }
-
- /**
- * Enabling EventEmitter interface on Gerrit.
- *
- * This will enable to signal across different parts of js code without relying on DOM,
- * including core to core, plugin to plugin and also core to plugin.
- *
- * @example
- *
- * // Emit this event from pluginA
- * Gerrit.install(pluginA => {
- * fetch("some-api").then(() => {
- * Gerrit.on("your-special-event", {plugin: pluginA});
- * });
- * });
- *
- * // Listen on your-special-event from pluginB
- * Gerrit.install(pluginB => {
- * Gerrit.on("your-special-event", ({plugin}) => {
- * // do something, plugin is pluginA
- * });
- * });
- */
- addListener(eventName: string, cb: EventCallback) {
- this.reportingService.trackApi(fakeApi, 'global', 'addListener');
- return this.eventEmitter.addListener(eventName, cb);
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- dispatch(eventName: string, detail: any) {
- this.reportingService.trackApi(fakeApi, 'global', 'dispatch');
- return this.eventEmitter.dispatch(eventName, detail);
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- emit(eventName: string, detail: any) {
- this.reportingService.trackApi(fakeApi, 'global', 'emit');
- return this.eventEmitter.emit(eventName, detail);
- }
-
- off(eventName: string, cb: EventCallback) {
- this.reportingService.trackApi(fakeApi, 'global', 'off');
- this.eventEmitter.off(eventName, cb);
- }
-
- on(eventName: string, cb: EventCallback) {
- this.reportingService.trackApi(fakeApi, 'global', 'on');
- return this.eventEmitter.on(eventName, cb);
- }
-
- once(eventName: string, cb: EventCallback) {
- this.reportingService.trackApi(fakeApi, 'global', 'once');
- return this.eventEmitter.once(eventName, cb);
- }
-
- removeAllListeners(eventName: string) {
- this.reportingService.trackApi(fakeApi, 'global', 'removeAllListeners');
- this.eventEmitter.removeAllListeners(eventName);
- }
-
- removeListener(eventName: string, cb: EventCallback) {
- this.reportingService.trackApi(fakeApi, 'global', 'removeListener');
- this.eventEmitter.removeListener(eventName, cb);
- }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.ts
deleted file mode 100644
index b906891..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright 2019 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import {getPluginLoader} from './gr-plugin-loader';
-import {resetPlugins} from '../../../test/test-utils';
-import {
- GerritInternal,
- _testOnly_getGerritInternalPluginApi,
-} from './gr-gerrit';
-import {stubRestApi} from '../../../test/test-utils';
-import {getAppContext} from '../../../services/app-context';
-import {GrJsApiInterface} from './gr-js-api-interface-element';
-import {SinonFakeTimers} from 'sinon';
-import {Timestamp} from '../../../api/rest-api';
-import {assert} from '@open-wc/testing';
-
-suite('gr-gerrit tests', () => {
- let element: GrJsApiInterface;
- let clock: SinonFakeTimers;
- let pluginApi: GerritInternal;
-
- setup(() => {
- clock = sinon.useFakeTimers();
-
- stubRestApi('getAccount').returns(
- Promise.resolve({name: 'Judy Hopps', registered_on: '' as Timestamp})
- );
- stubRestApi('send').returns(
- Promise.resolve({...new Response(), status: 200})
- );
- element = getAppContext().jsApiService as GrJsApiInterface;
- pluginApi = _testOnly_getGerritInternalPluginApi();
- });
-
- teardown(() => {
- clock.restore();
- element._removeEventCallbacks();
- resetPlugins();
- });
-
- suite('proxy methods', () => {
- test('Gerrit._isPluginEnabled proxy to getPluginLoader()', () => {
- const stubFn = sinon.stub();
- sinon
- .stub(getPluginLoader(), 'isPluginEnabled')
- .callsFake((...args) => stubFn(...args));
- pluginApi._isPluginEnabled('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
-
- test('Gerrit._isPluginLoaded proxy to getPluginLoader()', () => {
- const stubFn = sinon.stub();
- sinon
- .stub(getPluginLoader(), 'isPluginLoaded')
- .callsFake((...args) => stubFn(...args));
- pluginApi._isPluginLoaded('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
- });
-});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 353fc2a..9f2301d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -3,7 +3,6 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {getPluginLoader} from './gr-plugin-loader';
import {hasOwnProperty} from '../../../utils/common-util';
import {
ChangeInfo,
@@ -20,53 +19,23 @@
ShowRevisionActionsDetail,
} from './gr-js-api-types';
import {EventType, TargetElement} from '../../../api/plugin';
-import {DiffLayer, HighlightJS, ParsedChangeInfo} from '../../../types/types';
+import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {MenuLink} from '../../../api/admin';
import {Finalizable} from '../../../services/registry';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {Provider} from '../../../models/dependency';
const elements: {[key: string]: HTMLElement} = {};
const eventCallbacks: {[key: string]: EventCallback[]} = {};
export class GrJsApiInterface implements JsApiService, Finalizable {
- constructor(readonly reporting: ReportingService) {}
+ constructor(
+ private waitForPluginsToLoad: Provider<Promise<void>>,
+ readonly reporting: ReportingService
+ ) {}
finalize() {}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- handleEvent(type: EventType, detail: any) {
- getPluginLoader()
- .awaitPluginsLoaded()
- .then(() => {
- switch (type) {
- case EventType.HISTORY:
- this._handleHistory(detail);
- break;
- case EventType.SHOW_CHANGE:
- this._handleShowChange(detail);
- break;
- case EventType.COMMENT:
- this._handleComment(detail);
- break;
- case EventType.LABEL_CHANGE:
- this._handleLabelChange(detail);
- break;
- case EventType.SHOW_REVISION_ACTIONS:
- this._handleShowRevisionActions(detail);
- break;
- case EventType.HIGHLIGHTJS_LOADED:
- this._handleHighlightjsLoaded(detail);
- break;
- default:
- console.warn(
- 'handleEvent called with unsupported event type:',
- type
- );
- break;
- }
- });
- }
-
addElement(key: TargetElement, el: HTMLElement) {
elements[key] = el;
}
@@ -107,22 +76,8 @@
}
}
- // TODO(TS): The HISTORY event and its handler seem unused.
- _handleHistory(detail: {path: string}) {
- for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
- try {
- cb(detail.path);
- } catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('handleHistory callback error'),
- err
- );
- }
- }
- }
-
- _handleShowChange(detail: ShowChangeDetail) {
+ async handleShowChange(detail: ShowChangeDetail) {
+ await this.waitForPluginsToLoad();
// Note (issue 8221) Shallow clone the change object and add a mergeable
// getter with deprecation warning. This makes the change detail appear as
// though SKIP_MERGEABLE was not set, so that plugins that expect it can
@@ -167,7 +122,8 @@
}
}
- _handleShowRevisionActions(detail: ShowRevisionActionsDetail) {
+ async handleShowRevisionActions(detail: ShowRevisionActionsDetail) {
+ await this.waitForPluginsToLoad();
const registeredCallbacks = this._getEventCallbacks(
EventType.SHOW_REVISION_ACTIONS
);
@@ -198,22 +154,8 @@
}
}
- // TODO(TS): The COMMENT event and its handler seem unused.
- _handleComment(detail: {node: Node}) {
- for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
- try {
- cb(detail.node);
- } catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('comment callback error'),
- err
- );
- }
- }
- }
-
- _handleLabelChange(detail: {change: ChangeInfo}) {
+ async handleLabelChange(detail: {change?: ParsedChangeInfo}) {
+ await this.waitForPluginsToLoad();
for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
try {
cb(detail.change);
@@ -227,20 +169,6 @@
}
}
- _handleHighlightjsLoaded(detail: {hljs: HighlightJS}) {
- for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
- try {
- cb(detail.hljs);
- } catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('HighlightjsLoaded callback error'),
- err
- );
- }
- }
- }
-
modifyRevertMsg(change: ChangeInfo, revertMsg: string, origMsg: string) {
for (const cb of this._getEventCallbacks(EventType.REVERT)) {
try {
@@ -320,17 +248,15 @@
* will resolve to null.
*/
getCoverageAnnotationApis(): Promise<GrAnnotationActionsInterface[]> {
- return getPluginLoader()
- .awaitPluginsLoaded()
- .then(() => {
- const providers: GrAnnotationActionsInterface[] = [];
- this._getEventCallbacks(EventType.ANNOTATE_DIFF).forEach(cb => {
- const annotationApi = cb as unknown as GrAnnotationActionsInterface;
- const provider = annotationApi.getCoverageProvider();
- if (provider) providers.push(annotationApi);
- });
- return providers;
+ return this.waitForPluginsToLoad().then(() => {
+ const providers: GrAnnotationActionsInterface[] = [];
+ this._getEventCallbacks(EventType.ANNOTATE_DIFF).forEach(cb => {
+ const annotationApi = cb as unknown as GrAnnotationActionsInterface;
+ const provider = annotationApi.getCoverageProvider();
+ if (provider) providers.push(annotationApi);
});
+ return providers;
+ });
}
getAdminMenuLinks(): MenuLink[] {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts
index 240bc0b..2ec4f27 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.ts
@@ -5,4 +5,3 @@
*/
import './gr-js-api-interface-element';
import './gr-public-js-api';
-import './gr-gerrit';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 4fc403d..4ee5dc7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -8,19 +8,20 @@
import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface';
import {EventType} from '../../../api/plugin';
import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils';
-import {getPluginLoader} from './gr-plugin-loader';
import {
stubRestApi,
stubBaseUrl,
waitEventLoop,
} from '../../../test/test-utils';
-import {getAppContext} from '../../../services/app-context';
import {assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
+import {pluginLoaderToken} from './gr-plugin-loader';
suite('GrJsApiInterface tests', () => {
let element;
let plugin;
let errorStub;
+ let pluginLoader;
let sendStub;
let clock;
@@ -34,11 +35,12 @@
stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
- element = getAppContext().jsApiService;
+ pluginLoader = testResolver(pluginLoaderToken);
+ element = pluginLoader.jsApiService;
errorStub = sinon.stub(element.reporting, 'error');
- window.Gerrit.install(p => { plugin = p; }, '0.1',
+ pluginLoader.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- getPluginLoader().loadPlugins([]);
+ testResolver(pluginLoaderToken).loadPlugins([]);
});
teardown(() => {
@@ -69,17 +71,6 @@
});
});
- test('history event', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- plugin.on(EventType.HISTORY, throwErrFn);
- plugin.on(EventType.HISTORY, resolve);
- element.handleEvent(EventType.HISTORY, {path: '/path/to/awesomesauce'});
- const path = await promise;
- assert.equal(path, '/path/to/awesomesauce');
- assert.isTrue(errorStub.calledOnce);
- });
-
test('showchange event', async () => {
let resolve;
const promise = new Promise(r => resolve = r);
@@ -92,8 +83,9 @@
plugin.on(EventType.SHOW_CHANGE, (change, revision, info) => {
resolve({change, revision, info});
});
- element.handleEvent(EventType.SHOW_CHANGE,
- {change: testChange, patchNum: 1, info: {mergeable: false}});
+ element.handleShowChange(
+ {change: testChange, patchNum: 1, info: {mergeable: false}}
+ );
const {change, revision, info} = await promise;
assert.deepEqual(change, expectedChange);
@@ -113,8 +105,9 @@
plugin.on(EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
resolve({change, actions});
});
- element.handleEvent(EventType.SHOW_REVISION_ACTIONS,
- {change: testChange, revisionActions: {test: {}}});
+ element.handleShowRevisionActions(
+ {change: testChange, revisionActions: {test: {}}}
+ );
const {change, actions} = await promise;
assert.deepEqual(change, testChange);
@@ -122,16 +115,15 @@
assert.isTrue(errorStub.calledOnce);
});
- test('handleEvent awaits plugins load', async () => {
+ test('handleShowChange awaits plugins load', async () => {
const testChange = {
_number: 42,
revisions: {def: {_number: 2}, abc: {_number: 1}},
};
const spy = sinon.spy();
- getPluginLoader().loadPlugins(['plugins/test.js']);
+ testResolver(pluginLoaderToken).loadPlugins(['plugins/test.js']);
plugin.on(EventType.SHOW_CHANGE, spy);
- element.handleEvent(EventType.SHOW_CHANGE,
- {change: testChange, patchNum: 1});
+ element.handleShowChange({change: testChange, patchNum: 1});
assert.isFalse(spy.called);
// Timeout on loading plugins
@@ -141,19 +133,6 @@
assert.isTrue(spy.called);
});
- test('comment event', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- const testCommentNode = {foo: 'bar'};
- plugin.on(EventType.COMMENT, throwErrFn);
- plugin.on(EventType.COMMENT, resolve);
- element.handleEvent(EventType.COMMENT, {node: testCommentNode});
-
- const commentNode = await promise;
- assert.deepEqual(commentNode, testCommentNode);
- assert.isTrue(errorStub.calledOnce);
- });
-
test('revert event', () => {
function appendToRevertMsg(c, revertMsg, originalMsg) {
return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
@@ -225,7 +204,7 @@
const testChange = {_number: 42};
plugin.on(EventType.LABEL_CHANGE, throwErrFn);
plugin.on(EventType.LABEL_CHANGE, resolve);
- element.handleEvent(EventType.LABEL_CHANGE, {change: testChange});
+ element.handleLabelChange({change: testChange});
const change = await promise;
assert.deepEqual(change, testChange);
@@ -243,19 +222,6 @@
assert.isTrue(errorStub.calledTwice);
});
- test('highlightjs-loaded event', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- const testHljs = {_number: 42};
- plugin.on(EventType.HIGHLIGHTJS_LOADED, throwErrFn);
- plugin.on(EventType.HIGHLIGHTJS_LOADED, resolve);
- element.handleEvent(EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
-
- const hljs = await promise;
- assert.deepEqual(hljs, testHljs);
- assert.isTrue(errorStub.calledOnce);
- });
-
test('getLoggedIn', () => {
// fake fetch for authCheck
sinon.stub(window, 'fetch').callsFake(() => Promise.resolve({status: 204}));
@@ -289,7 +255,7 @@
setup(() => {
stubBaseUrl('/r');
- window.Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
+ pluginLoader.install(p => { baseUrlPlugin = p; }, '0.1',
'http://test.com/r/plugins/baseurlplugin/static/test.js');
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index 7aad2f0..c7a5eae 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -17,14 +17,14 @@
import {MenuLink} from '../../../api/admin';
export interface ShowChangeDetail {
- change: ChangeInfo;
- patchNum: PatchSetNum;
- info: {mergeable: boolean};
+ change?: ParsedChangeInfo;
+ patchNum?: PatchSetNum;
+ info: {mergeable: boolean | null};
}
export interface ShowRevisionActionsDetail {
change: ChangeInfo;
- revisionActions: {[key: string]: ActionInfo};
+ revisionActions: {[key: string]: ActionInfo | undefined};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -38,8 +38,9 @@
revertSubmissionMsg: string,
origMsg: string
): string;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- handleEvent(eventName: EventType, detail: any): void;
+ handleShowChange(detail: ShowChangeDetail): void;
+ handleShowRevisionActions(detail: ShowRevisionActionsDetail): void;
+ handleLabelChange(detail: {change?: ParsedChangeInfo}): void;
modifyRevertMsg(
change: ChangeInfo,
revertMsg: string,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
deleted file mode 100644
index a401351..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import './gr-js-api-interface';
-import {GrPluginActionContext} from './gr-plugin-action-context';
-import {addListenerForTest, waitEventLoop} from '../../../test/test-utils';
-import {EventType} from '../../../types/events';
-import {assert} from '@open-wc/testing';
-
-suite('gr-plugin-action-context tests', () => {
- let instance;
-
- let plugin;
-
- setup(() => {
- window.Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrPluginActionContext(plugin);
- });
-
- test('popup() and hide()', async () => {
- const popupApiStub = {
- _getElement: sinon.stub().returns(document.createElement('div')),
- close: sinon.stub(),
- };
- sinon.stub(plugin, 'popup').returns(Promise.resolve(popupApiStub));
- const el = document.createElement('span');
- instance.popup(el);
- await waitEventLoop();
- assert.isTrue(popupApiStub._getElement.called);
- instance.hide();
- assert.isTrue(popupApiStub.close.called);
- });
-
- test('textfield', () => {
- assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
- });
-
- test('br', () => {
- assert.equal(instance.br().tagName, 'BR');
- });
-
- test('msg', () => {
- const el = instance.msg('foobar');
- assert.equal(el.tagName, 'GR-LABEL');
- assert.equal(el.textContent, 'foobar');
- });
-
- test('div', () => {
- const el1 = document.createElement('span');
- el1.textContent = 'foo';
- const el2 = document.createElement('div');
- el2.textContent = 'bar';
- const div = instance.div(el1, el2);
- assert.equal(div.tagName, 'DIV');
- assert.equal(div.textContent, 'foobar');
- });
-
- suite('button', () => {
- let clickStub;
- let button;
- setup(() => {
- clickStub = sinon.stub();
- button = instance.button('foo', {onclick: clickStub});
- // If you don't attach a Polymer element to the DOM, then the ready()
- // callback will not be called and then e.g. this.$ is undefined.
- document.body.appendChild(button);
- });
-
- test('click', async () => {
- button.click();
- await waitEventLoop();
- assert.isTrue(clickStub.called);
- assert.equal(button.textContent, 'foo');
- });
-
- teardown(() => {
- button.remove();
- });
- });
-
- test('checkbox', () => {
- const el = instance.checkbox();
- assert.equal(el.tagName, 'INPUT');
- assert.equal(el.type, 'checkbox');
- });
-
- test('label', () => {
- const fakeMsg = {};
- const fakeCheckbox = {};
- sinon.stub(instance, 'div');
- sinon.stub(instance, 'msg').returns(fakeMsg);
- instance.label(fakeCheckbox, 'foo');
- assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
- });
-
- test('call', () => {
- instance.action = {
- method: 'METHOD',
- __key: 'key',
- __url: '/changes/1/revisions/2/foo~bar',
- };
- const sendStub = sinon.stub().returns(Promise.resolve());
- sinon.stub(plugin, 'restApi').returns({
- send: sendStub,
- });
- const payload = {foo: 'foo'};
- const successStub = sinon.stub();
- instance.call(payload, successStub);
- assert.isTrue(sendStub.calledWith(
- 'METHOD', '/changes/1/revisions/2/foo~bar', payload));
- });
-
- test('call error', async () => {
- instance.action = {
- method: 'METHOD',
- __key: 'key',
- __url: '/changes/1/revisions/2/foo~bar',
- };
- const sendStub = sinon.stub().returns(Promise.reject(new Error('boom')));
- sinon.stub(plugin, 'restApi').returns({
- send: sendStub,
- });
- const errorStub = sinon.stub();
- addListenerForTest(document, EventType.SHOW_ALERT, errorStub);
- instance.call();
- await waitEventLoop();
- assert.isTrue(errorStub.calledOnce);
- assert.equal(errorStub.args[0][0].detail.message,
- 'Plugin network error: Error: boom');
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
new file mode 100644
index 0000000..eaf6612
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
@@ -0,0 +1,161 @@
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-js-api-interface';
+import {GrPluginActionContext} from './gr-plugin-action-context';
+import {addListenerForTest, waitEventLoop} from '../../../test/test-utils';
+import {EventType} from '../../../types/events';
+import {assert} from '@open-wc/testing';
+import {PluginApi} from '../../../api/plugin';
+import {SinonStub, stub, spy} from 'sinon';
+import {PopupPluginApi} from '../../../api/popup';
+import {GrButton} from '../gr-button/gr-button';
+import {createChange, createRevision} from '../../../test/test-data-generators';
+import {ActionType} from '../../../api/change-actions';
+import {HttpMethod} from '../../../api/rest-api';
+import {RestPluginApi} from '../../../api/rest';
+
+suite('gr-plugin-action-context tests', () => {
+ let instance: GrPluginActionContext;
+
+ let plugin: PluginApi;
+
+ setup(() => {
+ window.Gerrit.install(
+ p => {
+ plugin = p;
+ },
+ '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js'
+ );
+ instance = new GrPluginActionContext(
+ plugin,
+ {
+ label: 'MyAction',
+ method: HttpMethod.POST,
+ __key: 'key',
+ __url: '/changes/1/revisions/2/foo~bar',
+ __type: ActionType.REVISION,
+ },
+ createChange(),
+ createRevision()
+ );
+ });
+
+ test('popup() and hide()', async () => {
+ const popupApiStub = {
+ _getElement: stub().returns(document.createElement('div')),
+ close: stub(),
+ } as PopupPluginApi & {_getElement: SinonStub; close: SinonStub};
+ stub(plugin, 'popup').resolves(popupApiStub);
+ const el = document.createElement('span');
+ instance.popup(el);
+ await waitEventLoop();
+ assert.isTrue(popupApiStub._getElement.called);
+ instance.hide();
+ assert.isTrue(popupApiStub.close.called);
+ });
+
+ test('textfield', () => {
+ assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
+ });
+
+ test('br', () => {
+ assert.equal(instance.br().tagName, 'BR');
+ });
+
+ test('msg', () => {
+ const el = instance.msg('foobar');
+ assert.equal(el.tagName, 'GR-LABEL');
+ assert.equal(el.textContent, 'foobar');
+ });
+
+ test('div', () => {
+ const el1 = document.createElement('span');
+ el1.textContent = 'foo';
+ const el2 = document.createElement('div');
+ el2.textContent = 'bar';
+ const div = instance.div(el1, el2);
+ assert.equal(div.tagName, 'DIV');
+ assert.equal(div.textContent, 'foobar');
+ });
+
+ suite('button', () => {
+ let clickStub: SinonStub;
+ let button: GrButton;
+ setup(() => {
+ clickStub = stub();
+ button = instance.button('foo', {onclick: clickStub});
+ // If you don't attach a Polymer element to the DOM, then the ready()
+ // callback will not be called and then e.g. this.$ is undefined.
+ document.body.appendChild(button);
+ });
+
+ test('click', async () => {
+ button.click();
+ await waitEventLoop();
+ assert.isTrue(clickStub.called);
+ assert.equal(button.textContent, 'foo');
+ });
+
+ teardown(() => {
+ button.remove();
+ });
+ });
+
+ test('checkbox', () => {
+ const el = instance.checkbox();
+ assert.equal(el.tagName, 'INPUT');
+ assert.equal(el.type, 'checkbox');
+ });
+
+ test('label', () => {
+ const divSpy = spy(instance, 'div');
+ const fakeMsg = document.createElement('gr-label');
+ const fakeCheckbox = document.createElement('input');
+ stub(instance, 'msg').returns(fakeMsg);
+
+ instance.label(fakeCheckbox, 'foo');
+
+ assert.isTrue(divSpy.calledWithExactly(fakeCheckbox, fakeMsg));
+ });
+
+ test('call', () => {
+ const fakeRestApi = {
+ send: stub().resolves(),
+ } as RestPluginApi & {send: SinonStub};
+ stub(plugin, 'restApi').returns(fakeRestApi);
+
+ const payload = {foo: 'foo'};
+ instance.call(payload, () => {});
+
+ assert.isTrue(
+ fakeRestApi.send.calledWith(
+ HttpMethod.POST,
+ '/changes/1/revisions/2/foo~bar',
+ payload
+ )
+ );
+ });
+
+ test('call error', async () => {
+ const fakeRestApi = {
+ send: () => Promise.reject(new Error('boom')),
+ } as unknown as RestPluginApi;
+ stub(plugin, 'restApi').returns(fakeRestApi);
+ const errorStub = stub();
+ addListenerForTest(document, EventType.SHOW_ALERT, errorStub);
+
+ instance.call({}, () => {});
+ await waitEventLoop();
+
+ assert.isTrue(errorStub.calledOnce);
+ assert.equal(
+ errorStub.args[0][0].detail.message,
+ 'Plugin network error: Error: boom'
+ );
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
index 9ced917..af744c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {PluginApi} from '../../../api/plugin';
-import {notUndefined} from '../../../types/types';
+import {isDefined} from '../../../types/types';
import {HookApi, PluginElement} from '../../../api/hook';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -161,17 +161,7 @@
return [];
}
return Array.from(new Set(modulesData.map(m => m.pluginUrl))).filter(
- notUndefined
+ isDefined
);
}
}
-
-let pluginEndpoints = new GrPluginEndpoints();
-
-// To avoid mutable-exports, we don't want to export above variable directly
-export function getPluginEndpoints() {
- return pluginEndpoints;
-}
-export function _testOnly_resetEndpoints() {
- pluginEndpoints = new GrPluginEndpoints();
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
index 15e19e6..2ef86ed 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
-import {resetPlugins} from '../../../test/test-utils';
import './gr-js-api-interface';
import {GrPluginEndpoints} from './gr-plugin-endpoints';
import {PluginApi} from '../../../api/plugin';
@@ -70,10 +69,6 @@
});
});
- teardown(() => {
- resetPlugins();
- });
-
test('getDetails all', () => {
assert.deepEqual(instance.getDetails('my-endpoint'), [
{
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 4a90314..b2894a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -3,7 +3,6 @@
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {getAppContext} from '../../../services/app-context';
import {
PLUGIN_LOADING_TIMEOUT_MS,
getPluginNameFromUrl,
@@ -12,10 +11,24 @@
} from './gr-api-utils';
import {Plugin} from './gr-public-js-api';
import {getBaseUrl} from '../../../utils/url-util';
-import {getPluginEndpoints} from './gr-plugin-endpoints';
+import {GrPluginEndpoints} from './gr-plugin-endpoints';
import {PluginApi} from '../../../api/plugin';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {fireAlert} from '../../../utils/event-util';
+import {JsApiService} from './gr-js-api-types';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {Finalizable} from '../../../services/registry';
+import {PluginsModel} from '../../../models/plugins/plugins-model';
+import {Gerrit} from '../../../api/gerrit';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {iconStyles} from '../../../styles/gr-icon-styles';
+import {GrJsApiInterface} from './gr-js-api-interface-element';
+import {define} from '../../../models/dependency';
enum PluginState {
/** State that indicates the plugin is pending to be loaded. */
@@ -55,6 +68,8 @@
// plugins with incompatible version will not be loaded.
const API_VERSION = '0.1';
+export const pluginLoaderToken = define<PluginLoader>('plugin-loader');
+
/**
* PluginLoader, responsible for:
*
@@ -64,32 +79,53 @@
* Retrieve plugin.
* Check plugin status and if all plugins loaded.
*/
-export class PluginLoader {
- _pluginListLoaded = false;
+export class PluginLoader implements Gerrit, Finalizable {
+ public readonly styles = {
+ font: fontStyles,
+ form: formStyles,
+ icon: iconStyles,
+ menuPage: menuPageStyles,
+ spinner: spinnerStyles,
+ subPage: subpageStyles,
+ table: tableStyles,
+ };
- _plugins = new Map<string, PluginObject>();
+ private pluginListLoaded = false;
- _reporting: ReportingService | null = null;
+ private plugins = new Map<string, PluginObject>();
// Promise that resolves when all plugins loaded
- _loadingPromise: Promise<void> | null = null;
+ private loadingPromise: Promise<void> | null = null;
- // Resolver to resolve _loadingPromise once all plugins loaded
- _loadingResolver: (() => void) | null = null;
+ // Resolver to resolve loadingPromise once all plugins loaded
+ private loadingResolver: (() => void) | null = null;
private instanceId?: string;
- _getReporting() {
- if (!this._reporting) {
- this._reporting = getAppContext().reportingService;
- }
- return this._reporting;
+ public readonly jsApiService: JsApiService;
+
+ public readonly pluginsModel: PluginsModel;
+
+ public pluginEndPoints: GrPluginEndpoints;
+
+ constructor(
+ private readonly reportingService: ReportingService,
+ private readonly restApiService: RestApiService
+ ) {
+ this.jsApiService = new GrJsApiInterface(
+ () => this.awaitPluginsLoaded(),
+ this.reportingService
+ );
+ this.pluginsModel = new PluginsModel();
+ this.pluginEndPoints = new GrPluginEndpoints();
}
+ finalize() {}
+
/**
* Use the plugin name or use the full url if not recognized.
*/
- _getPluginKeyFromUrl(url: string) {
+ private getPluginKeyFromUrl(url: string) {
return getPluginNameFromUrl(url) || `${UNKNOWN_PLUGIN_PREFIX}${url}`;
}
@@ -98,41 +134,41 @@
*/
loadPlugins(plugins: string[] = [], instanceId?: string) {
this.instanceId = instanceId;
- this._pluginListLoaded = true;
+ this.pluginListLoaded = true;
plugins.forEach(path => {
- const url = this._urlFor(path, window.ASSETS_PATH);
- const pluginKey = this._getPluginKeyFromUrl(url);
+ const url = this.urlFor(path, window.ASSETS_PATH);
+ const pluginKey = this.getPluginKeyFromUrl(url);
// Skip if already installed.
- if (this._plugins.has(pluginKey)) return;
- this._plugins.set(pluginKey, {
+ if (this.plugins.has(pluginKey)) return;
+ this.plugins.set(pluginKey, {
name: pluginKey,
url,
state: PluginState.PENDING,
plugin: null,
});
- if (this._isPathEndsWith(url, '.js')) {
- this._loadJsPlugin(path);
+ if (this.isPathEndsWith(url, '.js')) {
+ this.loadJsPlugin(path);
} else {
- this._failToLoad(`Unrecognized plugin path ${path}`, path);
+ this.failToLoad(`Unrecognized plugin path ${path}`, path);
}
});
this.awaitPluginsLoaded().then(() => {
const loaded = this.getPluginsByState(PluginState.LOADED);
const failed = this.getPluginsByState(PluginState.LOAD_FAILED);
- this._getReporting().pluginsLoaded(loaded.map(p => p.name));
- this._getReporting().pluginsFailed(failed.map(p => p.name));
+ this.reportingService.pluginsLoaded(loaded.map(p => p.name));
+ this.reportingService.pluginsFailed(failed.map(p => p.name));
});
}
- _isPathEndsWith(url: string | URL, suffix: string) {
+ private isPathEndsWith(url: string | URL, suffix: string) {
if (!(url instanceof URL)) {
try {
url = new URL(url);
} catch (e: unknown) {
- this._getReporting().error(
+ this.reportingService.error(
'GrPluginLoader',
new Error('url parse error'),
e
@@ -145,7 +181,7 @@
}
private getPluginsByState(state: PluginState) {
- return [...this._plugins.values()].filter(p => p.state === state);
+ return [...this.plugins.values()].filter(p => p.state === state);
}
install(
@@ -163,31 +199,38 @@
src = script && script.baseURI;
}
if (!src) {
- this._failToLoad('Failed to determine src.');
+ this.failToLoad('Failed to determine src.');
return;
}
if (version && version !== API_VERSION) {
- this._failToLoad(
+ this.failToLoad(
`Plugin ${src} install error: only version ${API_VERSION} is supported in PolyGerrit. ${version} was given.`,
src
);
return;
}
- const url = this._urlFor(src);
+ const url = this.urlFor(src);
const pluginObject = this.getPlugin(url);
let plugin = pluginObject && pluginObject.plugin;
if (!plugin) {
- plugin = new Plugin(url);
+ plugin = new Plugin(
+ url,
+ this.jsApiService,
+ this.reportingService,
+ this.restApiService,
+ this.pluginsModel,
+ this.pluginEndPoints
+ );
}
try {
callback(plugin);
- this._pluginInstalled(url, plugin);
+ this.pluginInstalled(url, plugin);
} catch (e: unknown) {
if (e instanceof Error) {
- this._failToLoad(`${e.name}: ${e.message}`, src);
+ this.failToLoad(`${e.name}: ${e.message}`, src);
} else {
- this._getReporting().error(
+ this.reportingService.error(
'GrPluginLoader',
new Error('plugin callback error'),
e
@@ -197,27 +240,27 @@
}
arePluginsLoaded() {
- if (!this._pluginListLoaded) return false;
+ if (!this.pluginListLoaded) return false;
return this.getPluginsByState(PluginState.PENDING).length === 0;
}
- _checkIfCompleted() {
+ private checkIfCompleted() {
if (this.arePluginsLoaded()) {
- getPluginEndpoints().setPluginsReady();
- if (this._loadingResolver) {
- this._loadingResolver();
- this._loadingResolver = null;
- this._loadingPromise = null;
+ this.pluginEndPoints.setPluginsReady();
+ if (this.loadingResolver) {
+ this.loadingResolver();
+ this.loadingResolver = null;
+ this.loadingPromise = null;
}
}
}
- _timeout() {
+ private timeout() {
const pending = this.getPluginsByState(PluginState.PENDING);
for (const plugin of pending) {
- this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
+ this.updatePluginState(plugin.url, PluginState.LOAD_FAILED);
}
- this._checkIfCompleted();
+ this.checkIfCompleted();
const errorMessage = `Timeout when loading plugins: ${pending
.map(p => p.name)
.join(',')}`;
@@ -225,21 +268,25 @@
return errorMessage;
}
- _failToLoad(message: string, pluginUrl?: string) {
+ // Private but mocked in tests.
+ failToLoad(message: string, pluginUrl?: string) {
// Show an alert with the error
fireAlert(document, `Plugin install error: ${message} from ${pluginUrl}`);
- if (pluginUrl) this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
- this._checkIfCompleted();
+ if (pluginUrl) this.updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
+ this.checkIfCompleted();
}
- _updatePluginState(pluginUrl: string, state: PluginState): PluginObject {
- const key = this._getPluginKeyFromUrl(pluginUrl);
- if (this._plugins.has(key)) {
- this._plugins.get(key)!.state = state;
+ private updatePluginState(
+ pluginUrl: string,
+ state: PluginState
+ ): PluginObject {
+ const key = this.getPluginKeyFromUrl(pluginUrl);
+ if (this.plugins.has(key)) {
+ this.plugins.get(key)!.state = state;
} else {
// Plugin is not recorded for some reason.
console.info(`Plugin loaded separately: ${pluginUrl}`);
- this._plugins.set(key, {
+ this.plugins.set(key, {
name: key,
url: pluginUrl,
state,
@@ -247,59 +294,61 @@
});
}
console.debug(`Plugin ${key} ${state}`);
- return this._plugins.get(key)!;
+ return this.plugins.get(key)!;
}
- _pluginInstalled(url: string, plugin: PluginApi) {
- const pluginObj = this._updatePluginState(url, PluginState.LOADED);
+ private pluginInstalled(url: string, plugin: PluginApi) {
+ const pluginObj = this.updatePluginState(url, PluginState.LOADED);
pluginObj.plugin = plugin;
- this._getReporting().pluginLoaded(plugin.getPluginName() || url);
- this._checkIfCompleted();
+ this.reportingService.pluginLoaded(plugin.getPluginName() || url);
+ this.checkIfCompleted();
}
/**
* Checks if given plugin path/url is enabled or not.
*/
isPluginEnabled(pathOrUrl: string) {
- const url = this._urlFor(pathOrUrl);
- const key = this._getPluginKeyFromUrl(url);
- return this._plugins.has(key);
+ const url = this.urlFor(pathOrUrl);
+ const key = this.getPluginKeyFromUrl(url);
+ return this.plugins.has(key);
}
/**
* Returns the plugin object with a given url.
*/
getPlugin(pathOrUrl: string) {
- const url = this._urlFor(pathOrUrl);
- const key = this._getPluginKeyFromUrl(url);
- return this._plugins.get(key);
+ const url = this.urlFor(pathOrUrl);
+ const key = this.getPluginKeyFromUrl(url);
+ return this.plugins.get(key);
}
/**
* Checks if given plugin path/url is loaded or not.
*/
isPluginLoaded(pathOrUrl: string): boolean {
- const url = this._urlFor(pathOrUrl);
- const key = this._getPluginKeyFromUrl(url);
- return this._plugins.has(key)
- ? this._plugins.get(key)!.state === PluginState.LOADED
+ const url = this.urlFor(pathOrUrl);
+ const key = this.getPluginKeyFromUrl(url);
+ return this.plugins.has(key)
+ ? this.plugins.get(key)!.state === PluginState.LOADED
: false;
}
- _loadJsPlugin(pluginUrl: string) {
- const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
- const urlWithoutAP = this._urlFor(pluginUrl);
+ // Private but mocked in tests.
+ loadJsPlugin(pluginUrl: string) {
+ const urlWithAP = this.urlFor(pluginUrl, window.ASSETS_PATH);
+ const urlWithoutAP = this.urlFor(pluginUrl);
let onerror = undefined;
if (urlWithAP !== urlWithoutAP) {
- onerror = () => this._createScriptTag(urlWithoutAP);
+ onerror = () => this.createScriptTag(urlWithoutAP);
}
- this._createScriptTag(urlWithAP, onerror);
+ this.createScriptTag(urlWithAP, onerror);
}
- _createScriptTag(url: string, onerror?: OnErrorEventHandler) {
+ // Private but mocked in tests.
+ createScriptTag(url: string, onerror?: OnErrorEventHandler) {
if (!onerror) {
- onerror = () => this._failToLoad(`${url} load error`, url);
+ onerror = () => this.failToLoad(`${url} load error`, url);
}
const el = document.createElement('script');
@@ -313,7 +362,7 @@
return document.body.appendChild(el);
}
- _urlFor(pathOrUrl: string, assetsPath?: string): string {
+ private urlFor(pathOrUrl: string, assetsPath?: string): string {
if (isThemeFile(pathOrUrl)) {
if (assetsPath && this.instanceId) {
return `${assetsPath}/hosts/${this.instanceId}${THEME_JS}`;
@@ -341,39 +390,28 @@
awaitPluginsLoaded() {
// Resolve if completed.
- this._checkIfCompleted();
+ this.checkIfCompleted();
if (this.arePluginsLoaded()) {
return Promise.resolve();
}
- if (!this._loadingPromise) {
+ if (!this.loadingPromise) {
// specify window here so that TS pulls the correct setTimeout method
// if window is not specified, then the function is pulled from node
// and the return type is NodeJS.Timeout object
let timerId: number;
- this._loadingPromise = Promise.race([
- new Promise<void>(resolve => (this._loadingResolver = resolve)),
+ this.loadingPromise = Promise.race([
+ new Promise<void>(resolve => (this.loadingResolver = resolve)),
new Promise(
(_, reject) =>
(timerId = window.setTimeout(() => {
- reject(new Error(this._timeout()));
+ reject(new Error(this.timeout()));
}, PLUGIN_LOADING_TIMEOUT_MS))
),
]).finally(() => {
if (timerId) clearTimeout(timerId);
}) as Promise<void>;
}
- return this._loadingPromise;
+ return this.loadingPromise;
}
}
-
-// TODO(dmfilippov): Convert to service and add to appContext
-let pluginLoader = new PluginLoader();
-export function _testOnly_resetPluginLoader() {
- pluginLoader = new PluginLoader();
- return pluginLoader;
-}
-
-export function getPluginLoader() {
- return pluginLoader;
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
index 3005c37..acce236 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
@@ -5,18 +5,15 @@
*/
import '../../../test/common-test-setup';
import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils';
-import {PluginLoader, _testOnly_resetPluginLoader} from './gr-plugin-loader';
-import {
- resetPlugins,
- stubBaseUrl,
- waitEventLoop,
-} from '../../../test/test-utils';
+import {PluginLoader} from './gr-plugin-loader';
+import {stubBaseUrl, waitEventLoop} from '../../../test/test-utils';
import {addListenerForTest, stubRestApi} from '../../../test/test-utils';
import {PluginApi} from '../../../api/plugin';
import {SinonFakeTimers} from 'sinon';
import {Timestamp} from '../../../api/rest-api';
import {EventType} from '../../../types/events';
import {assert} from '@open-wc/testing';
+import {getAppContext} from '../../../services/app-context';
suite('gr-plugin-loader tests', () => {
let plugin: PluginApi;
@@ -35,18 +32,20 @@
stubRestApi('send').returns(
Promise.resolve({...new Response(), status: 200})
);
- pluginLoader = _testOnly_resetPluginLoader();
+ pluginLoader = new PluginLoader(
+ getAppContext().reportingService,
+ getAppContext().restApiService
+ );
bodyStub = sinon.stub(document.body, 'appendChild');
url = window.location.origin;
});
teardown(() => {
clock.restore();
- resetPlugins();
});
test('reuse plugin for install calls', () => {
- window.Gerrit.install(
+ pluginLoader.install(
p => {
plugin = p;
},
@@ -55,7 +54,7 @@
);
let otherPlugin;
- window.Gerrit.install(
+ pluginLoader.install(
p => {
otherPlugin = p;
},
@@ -67,17 +66,17 @@
test('versioning', () => {
const callback = sinon.spy();
- window.Gerrit.install(callback, '0.0pre-alpha');
+ pluginLoader.install(callback, '0.0pre-alpha');
assert(callback.notCalled);
});
test('report pluginsLoaded', async () => {
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
pluginsLoadedStub.reset();
- (window.Gerrit as any)._loadPlugins([]);
+ pluginLoader.loadPlugins([]);
await waitEventLoop();
assert.isTrue(pluginsLoadedStub.called);
});
@@ -99,11 +98,11 @@
});
test('plugins installed successfully', async () => {
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(() => void 0, undefined, url);
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
@@ -119,8 +118,8 @@
});
test('isPluginEnabled and isPluginLoaded', async () => {
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(() => void 0, undefined, url);
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(() => void 0, undefined, url);
});
const plugins = [
@@ -147,8 +146,8 @@
const alertStub = sinon.stub();
addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(
() => {
if (url === plugins[0]) {
throw new Error('failed');
@@ -160,7 +159,7 @@
});
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
@@ -181,8 +180,8 @@
const alertStub = sinon.stub();
addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(
() => {
if (url === plugins[0]) {
throw new Error('failed');
@@ -194,7 +193,7 @@
});
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
@@ -220,8 +219,8 @@
const alertStub = sinon.stub();
addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(
() => {
throw new Error('failed');
},
@@ -231,7 +230,7 @@
});
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
@@ -252,12 +251,12 @@
const alertStub = sinon.stub();
addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(() => {}, url === plugins[0] ? '' : 'alpha', url);
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(() => {}, url === plugins[0] ? '' : 'alpha', url);
});
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
@@ -270,11 +269,11 @@
});
test('multiple assets for same plugin installed successfully', async () => {
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(() => void 0, undefined, url);
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sinon.stub(
- pluginLoader._getReporting(),
+ getAppContext().reportingService,
'pluginsLoaded'
);
@@ -295,7 +294,7 @@
setup(() => {
loadJsPluginStub = sinon.stub();
sinon
- .stub(pluginLoader, '_createScriptTag')
+ .stub(pluginLoader, 'createScriptTag')
.callsFake((url: string, _onerror?: OnErrorEventHandler | undefined) =>
loadJsPluginStub(url)
);
@@ -303,7 +302,7 @@
test('invalid plugin path', () => {
const failToLoadStub = sinon.stub();
- sinon.stub(pluginLoader, '_failToLoad').callsFake((...args) => {
+ sinon.stub(pluginLoader, 'failToLoad').callsFake((...args) => {
failToLoadStub(...args);
});
@@ -353,7 +352,7 @@
window.ASSETS_PATH = 'https://cdn.com';
loadJsPluginStub = sinon.stub();
sinon
- .stub(pluginLoader, '_createScriptTag')
+ .stub(pluginLoader, 'createScriptTag')
.callsFake((url: string, _onerror?: OnErrorEventHandler | undefined) =>
loadJsPluginStub(url)
);
@@ -409,8 +408,8 @@
installed = true;
}
}
- sinon.stub(pluginLoader, '_loadJsPlugin').callsFake(url => {
- window.Gerrit.install(() => pluginCallback(url), undefined, url);
+ sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
+ pluginLoader.install(() => pluginCallback(url), undefined, url);
});
pluginLoader.loadPlugins(plugins);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
index b4f7324..65e4960 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
@@ -5,9 +5,10 @@
*/
import {HttpMethod} from '../../../constants/constants';
import {RequestPayload} from '../../../types/common';
-import {getAppContext} from '../../../services/app-context';
import {ErrorCallback, RestPluginApi} from '../../../api/rest';
import {PluginApi} from '../../../api/plugin';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
async function getErrorMessage(response: Response): Promise<string> {
const text = await response.text();
@@ -24,11 +25,12 @@
}
export class GrPluginRestApi implements RestPluginApi {
- private readonly restApi = getAppContext().restApiService;
-
- private readonly reporting = getAppContext().reportingService;
-
- constructor(readonly plugin: PluginApi, private readonly prefix = '') {
+ constructor(
+ private readonly restApi: RestApiService,
+ private readonly reporting: ReportingService,
+ readonly plugin: PluginApi,
+ private readonly prefix = ''
+ ) {
this.reporting.trackApi(this.plugin, 'rest', 'constructor');
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
index d6d7fc2..c5bef85 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
@@ -14,6 +14,7 @@
createServerInfo,
} from '../../../test/test-data-generators';
import {HttpMethod} from '../../../api/rest-api';
+import {getAppContext} from '../../../services/app-context';
suite('gr-plugin-rest-api tests', () => {
let instance: GrPluginRestApi;
@@ -32,7 +33,11 @@
'0.1',
'http://test.com/plugins/testplugin/static/test.js'
);
- instance = new GrPluginRestApi(pluginApi!);
+ instance = new GrPluginRestApi(
+ getAppContext().restApiService,
+ getAppContext().reportingService,
+ pluginApi!
+ );
});
test('fetch', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index 21ab10a..6e638bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -13,7 +13,7 @@
import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
import {GrEventHelper} from '../../plugins/gr-event-helper/gr-event-helper';
import {GrPluginRestApi} from './gr-plugin-rest-api';
-import {getPluginEndpoints} from './gr-plugin-endpoints';
+import {GrPluginEndpoints} from './gr-plugin-endpoints';
import {getPluginNameFromUrl, send} from './gr-api-utils';
import {GrReportingJsApi} from './gr-reporting-js-api';
import {EventType, PluginApi, TargetElement} from '../../../api/plugin';
@@ -21,7 +21,6 @@
import {HttpMethod} from '../../../constants/constants';
import {GrChangeActions} from '../../change/gr-change-actions/gr-change-actions';
import {GrChecksApi} from '../../plugins/gr-checks-api/gr-checks-api';
-import {getAppContext} from '../../../services/app-context';
import {AdminPluginApi} from '../../../api/admin';
import {AnnotationPluginApi} from '../../../api/annotation';
import {EventHelperPluginApi} from '../../../api/event-helper';
@@ -32,6 +31,10 @@
import {RestPluginApi} from '../../../api/rest';
import {HookApi, PluginElement, RegisterOptions} from '../../../api/hook';
import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
+import {JsApiService} from './gr-js-api-types';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {PluginsModel} from '../../../models/plugins/plugins-model';
/**
* Plugin-provided custom components can affect content in extension
@@ -60,13 +63,14 @@
private readonly _name: string = PLUGIN_NAME_NOT_SET;
- private readonly jsApi = getAppContext().jsApiService;
-
- private readonly report = getAppContext().reportingService;
-
- private readonly restApiService = getAppContext().restApiService;
-
- constructor(url?: string) {
+ constructor(
+ url: string,
+ private readonly jsApi: JsApiService,
+ private readonly report: ReportingService,
+ private readonly restApiService: RestApiService,
+ private readonly pluginsModel: PluginsModel,
+ private readonly pluginEndpoints: GrPluginEndpoints
+ ) {
this.domHooks = new GrDomHooksManager(this);
if (!url) {
@@ -93,7 +97,7 @@
`The deprecated plugin API 'registerStyleModule()' was called with parameters '${endpoint}' and '${moduleName}'.`
);
this.report.trackApi(this, 'plugin', 'registerStyleModule');
- getPluginEndpoints().registerModule(this, {
+ this.pluginEndpoints.registerModule(this, {
endpoint,
type: EndpointType.STYLE,
moduleName,
@@ -145,7 +149,7 @@
const slot = options?.slot ?? '';
const domHook = this.domHooks.getDomHook<T>(endpoint, moduleName);
moduleName = moduleName || domHook.getModuleName();
- getPluginEndpoints().registerModule(this, {
+ this.pluginEndpoints.registerModule(this, {
slot,
endpoint,
type,
@@ -217,6 +221,7 @@
changeActions(): ChangeActionsPluginApi {
return new GrChangeActionsInterface(
this,
+ this.jsApi,
this.jsApi.getElement(
TargetElement.CHANGE_ACTIONS
) as unknown as GrChangeActions
@@ -228,27 +233,27 @@
}
checks(): GrChecksApi {
- return new GrChecksApi(this);
+ return new GrChecksApi(this.report, this.pluginsModel, this);
}
reporting(): ReportingPluginApi {
- return new GrReportingJsApi(this);
+ return new GrReportingJsApi(this.report, this);
}
admin(): AdminPluginApi {
- return new GrAdminApi(this);
+ return new GrAdminApi(this.report, this);
}
restApi(prefix?: string): RestPluginApi {
- return new GrPluginRestApi(this, prefix);
+ return new GrPluginRestApi(this.restApiService, this.report, this, prefix);
}
attributeHelper(element: HTMLElement): AttributeHelperPluginApi {
- return new GrAttributeHelper(this, element);
+ return new GrAttributeHelper(this.report, this, element);
}
eventHelper(element: HTMLElement): EventHelperPluginApi {
- return new GrEventHelper(this, element);
+ return new GrEventHelper(this.report, this, element);
}
popup(): Promise<PopupPluginApi>;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts
index fab0e6c..d82b68d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts
@@ -3,17 +3,18 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {getAppContext} from '../../../services/app-context';
import {PluginApi} from '../../../api/plugin';
import {EventDetails, ReportingPluginApi} from '../../../api/reporting';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
/**
* Defines all methods that will be exported to plugin from reporting service.
*/
export class GrReportingJsApi implements ReportingPluginApi {
- private readonly reporting = getAppContext().reportingService;
-
- constructor(private readonly plugin: PluginApi) {
+ constructor(
+ private readonly reporting: ReportingService,
+ private readonly plugin: PluginApi
+ ) {
this.reporting.trackApi(this.plugin, 'reporting', 'constructor');
}
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
index c18a31d..ea5540e 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -11,7 +11,6 @@
import {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior';
import {findActiveElement} from '../../../utils/dom-util';
import {fireEvent} from '../../../utils/event-util';
-import {getHovercardContainer} from '../../../mixins/hovercard-mixin/hovercard-mixin';
import {getFocusableElements} from '../../../utils/focusable';
const AWAIT_MAX_ITERS = 10;
@@ -110,20 +109,6 @@
}
}
- override _onCaptureFocus(e: Event) {
- const hovercardContainer = getHovercardContainer();
- if (hovercardContainer) {
- // Hovercard container is not a child of an overlay.
- // When an overlay is opened and a user clicks inside hovercard,
- // the IronOverlayBehavior doesn't allow to set focus inside a hovercard.
- // As a result, user can't select a text (username) in the hovercard
- // in a dialog. We should skip default _onCaptureFocus for hovercards.
- const path = e.composedPath();
- if (path.indexOf(hovercardContainer) >= 0) return;
- }
- super._onCaptureFocus(e);
- }
-
/**
* Override the focus stops that iron-overlay-behavior tries to find.
*/
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 9c54349..1fbe5b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -102,7 +102,7 @@
get(key: '/accounts/self/emails'): EmailInfo[] | null;
- get(key: '/accounts/self/detail'): AccountDetailInfo[] | null;
+ get(key: '/accounts/self/detail'): AccountDetailInfo | null;
get(key: string): ParsedJSON | null;
@@ -112,7 +112,7 @@
set(key: '/accounts/self/emails', value: EmailInfo[]): void;
- set(key: '/accounts/self/detail', value: AccountDetailInfo[]): void;
+ set(key: '/accounts/self/detail', value: AccountDetailInfo): void;
set(key: string, value: ParsedJSON | null): void;
@@ -275,16 +275,14 @@
* by this method, it should be called immediately after the request
* finishes.
*
+ * Private, but used in tests.
+ *
* @param startTime the time that the request was started.
* @param status the HTTP status of the response. The status value
* is used here rather than the response object so there is no way this
* method can read the body stream.
*/
- private _logCall(
- req: FetchRequest,
- startTime: number,
- status: number | null
- ) {
+ _logCall(req: FetchRequest, startTime: number, status: number | null) {
const method =
req.fetchOptions && req.fetchOptions.method
? req.fetchOptions.method
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
index 712ece4..1234b59 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
@@ -9,14 +9,15 @@
FetchPromisesCache,
GrRestApiHelper,
} from './gr-rest-api-helper';
-import {getAppContext} from '../../../../services/app-context';
-import {stubAuth, waitEventLoop} from '../../../../test/test-utils';
+import {waitEventLoop} from '../../../../test/test-utils';
import {FakeScheduler} from '../../../../services/scheduler/fake-scheduler';
import {RetryScheduler} from '../../../../services/scheduler/retry-scheduler';
import {ParsedJSON} from '../../../../types/common';
import {HttpMethod} from '../../../../api/rest-api';
import {SinonFakeTimers} from 'sinon';
import {assert} from '@open-wc/testing';
+import {AuthService} from '../../../../services/gr-auth/gr-auth';
+import {GrAuthMock} from '../../../../services/gr-auth/gr-auth_mock';
function makeParsedJSON<T>(val: T): ParsedJSON {
return val as unknown as ParsedJSON;
@@ -32,6 +33,7 @@
let authFetchStub: sinon.SinonStub;
let readScheduler: FakeScheduler<Response>;
let writeScheduler: FakeScheduler<Response>;
+ let authService: AuthService;
setup(() => {
clock = sinon.useFakeTimers();
@@ -42,7 +44,8 @@
window.CANONICAL_PATH = 'testhelper';
const testJSON = ')]}\'\n{"hello": "bonjour"}';
- authFetchStub = stubAuth('fetch').returns(
+ authService = new GrAuthMock();
+ authFetchStub = sinon.stub(authService, 'fetch').returns(
Promise.resolve({
...new Response(),
ok: true,
@@ -57,7 +60,7 @@
helper = new GrRestApiHelper(
cache,
- getAppContext().authService,
+ authService,
fetchPromisesCache,
readScheduler,
writeScheduler
@@ -270,7 +273,7 @@
test('are retried', async () => {
helper = new GrRestApiHelper(
cache,
- getAppContext().authService,
+ authService,
fetchPromisesCache,
new RetryScheduler<Response>(readScheduler, 1, 50),
writeScheduler
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 9a114e6..b38d71b 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -302,14 +302,16 @@
return this.textarea!.textarea;
}
+ override focus() {
+ this.textarea?.textarea.focus();
+ }
+
putCursorAtEnd() {
const textarea = this.getNativeTextarea();
// Put the cursor at the end always.
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.selectionStart;
- setTimeout(() => {
- textarea.focus();
- });
+ textarea.focus();
}
private getVisibleDropdown() {
@@ -343,7 +345,7 @@
e.preventDefault();
e.stopPropagation();
this.getVisibleDropdown().cursorUp();
- this.textarea!.textarea.focus();
+ this.focus();
}
private handleDownKey(e: KeyboardEvent) {
@@ -353,7 +355,7 @@
e.preventDefault();
e.stopPropagation();
this.getVisibleDropdown().cursorDown();
- this.textarea!.textarea.focus();
+ this.focus();
}
private handleTabKey(e: KeyboardEvent) {
@@ -568,7 +570,7 @@
async handleTextChanged() {
await this.computeSuggestions();
this.openOrResetDropdown();
- this.textarea!.textarea.focus();
+ this.focus();
}
private openEmojiDropdown() {
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 78c8aa3..634c387 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
@@ -633,12 +633,6 @@
});
suite('gr-textarea monospace', () => {
- // gr-textarea set monospace class in the ready() method.
- // In Polymer2, ready() is called from the fixture(...) method,
- // If ready() is called again later, some nested elements doesn't
- // handle it correctly. A separate test-fixture is used to set
- // properties before ready() is called.
-
let element: GrTextarea;
setup(async () => {
@@ -654,12 +648,6 @@
});
suite('gr-textarea hideBorder', () => {
- // gr-textarea set noBorder class in the ready() method.
- // In Polymer2, ready() is called from the fixture(...) method,
- // If ready() is called again later, some nested elements doesn't
- // handle it correctly. A separate test-fixture is used to set
- // properties before ready() is called.
-
let element: GrTextarea;
setup(async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 0e6b19e..4ccc635 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -5,7 +5,6 @@
*/
import '../gr-icon/gr-icon';
import '../gr-tooltip/gr-tooltip';
-import {getRootElement} from '../../../scripts/rootElement';
import {GrTooltip} from '../gr-tooltip/gr-tooltip';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
@@ -142,7 +141,7 @@
// Set visibility to hidden before appending to the DOM so that
// calculations can be made based on the element’s size.
tooltip.style.visibility = 'hidden';
- getRootElement().appendChild(tooltip);
+ document.body.appendChild(tooltip);
await tooltip.updateComplete;
this._positionTooltip(tooltip);
tooltip.style.visibility = 'initial';
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
new file mode 100644
index 0000000..d1478b3
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../elements/shared/gr-button/gr-button';
+import {html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {DiffInfo, DiffViewMode, RenderPreferences} from '../../../api/diff';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+import {getShowConfig} from './gr-context-controls';
+import {ifDefined} from 'lit/directives/if-defined.js';
+
+@customElement('gr-context-controls-section')
+export class GrContextControlsSection extends LitElement {
+ /** Should context controls be rendered for expanding above the section? */
+ @property({type: Boolean}) showAbove = false;
+
+ /** Should context controls be rendered for expanding below the section? */
+ @property({type: Boolean}) showBelow = false;
+
+ @property({type: Object}) viewMode = DiffViewMode.SIDE_BY_SIDE;
+
+ /** Must be of type GrDiffGroupType.CONTEXT_CONTROL. */
+ @property({type: Object})
+ group?: GrDiffGroup;
+
+ @property({type: Object})
+ diff?: DiffInfo;
+
+ @property({type: Object})
+ renderPrefs?: RenderPreferences;
+
+ /**
+ * Semantic DOM diff testing does not work with just table fragments, so when
+ * running such tests the render() method has to wrap the DOM in a proper
+ * <table> element.
+ */
+ @state()
+ addTableWrapperForTesting = false;
+
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ private renderPaddingRow(whereClass: 'above' | 'below') {
+ if (!this.showAbove && whereClass === 'above') return;
+ if (!this.showBelow && whereClass === 'below') return;
+ const sideBySide = this.viewMode === DiffViewMode.SIDE_BY_SIDE;
+ const modeClass = sideBySide ? 'side-by-side' : 'unified';
+ const type = sideBySide ? GrDiffGroupType.CONTEXT_CONTROL : undefined;
+ return html`
+ <tr
+ class=${diffClasses('contextBackground', modeClass, whereClass)}
+ left-type=${ifDefined(type)}
+ right-type=${ifDefined(type)}
+ >
+ <td class=${diffClasses('blame')} data-line-number="0"></td>
+ <td class=${diffClasses('contextLineNum')}></td>
+ ${sideBySide ? html`<td class=${diffClasses()}></td>` : ''}
+ <td class=${diffClasses('contextLineNum')}></td>
+ <td class=${diffClasses()}></td>
+ </tr>
+ `;
+ }
+
+ private createContextControlRow() {
+ const sideBySide = this.viewMode === DiffViewMode.SIDE_BY_SIDE;
+ const showConfig = getShowConfig(this.showAbove, this.showBelow);
+ return html`
+ <tr class=${diffClasses('dividerRow', `show-${showConfig}`)}>
+ <td class=${diffClasses('blame')} data-line-number="0"></td>
+ ${sideBySide ? html`<td class=${diffClasses()}></td>` : ''}
+ <td class=${diffClasses('dividerCell')} colspan="3">
+ <gr-context-controls
+ .diff=${this.diff}
+ .renderPreferences=${this.renderPrefs}
+ .group=${this.group}
+ .showConfig=${showConfig}
+ >
+ </gr-context-controls>
+ </td>
+ </tr>
+ `;
+ }
+
+ override render() {
+ const rows = html`
+ ${this.renderPaddingRow('above')} ${this.createContextControlRow()}
+ ${this.renderPaddingRow('below')}
+ `;
+ if (this.addTableWrapperForTesting) {
+ return html`<table>
+ ${rows}
+ </table>`;
+ }
+ return rows;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-context-controls-section': GrContextControlsSection;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
new file mode 100644
index 0000000..aa29ac3
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-context-controls-section';
+import {GrContextControlsSection} from './gr-context-controls-section';
+import {fixture, html, assert} from '@open-wc/testing';
+
+suite('gr-context-controls-section test', () => {
+ let element: GrContextControlsSection;
+
+ setup(async () => {
+ element = await fixture<GrContextControlsSection>(
+ html`<gr-context-controls-section></gr-context-controls-section>`
+ );
+ element.addTableWrapperForTesting = true;
+ await element.updateComplete;
+ });
+
+ test('render: normal with showAbove and showBelow', async () => {
+ element.showAbove = true;
+ element.showBelow = true;
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="above contextBackground gr-diff side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff"></td>
+ </tr>
+ <tr class="dividerRow gr-diff show-both">
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="gr-diff"></td>
+ <td class="dividerCell gr-diff" colspan="3">
+ <gr-context-controls showconfig="both"> </gr-context-controls>
+ </td>
+ </tr>
+ <tr
+ class="below contextBackground gr-diff side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff"></td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index a0d06f6..1679ee0 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -69,6 +69,19 @@
export type GrContextControlsShowConfig = 'above' | 'below' | 'both';
+export function getShowConfig(
+ showAbove: boolean,
+ showBelow: boolean
+): GrContextControlsShowConfig {
+ if (showAbove && !showBelow) return 'above';
+ if (!showAbove && showBelow) return 'below';
+
+ // Note that !showAbove && !showBelow also intentionally returns 'both'.
+ // This means the file is completely collapsed, which is unusual, but at least
+ // happens in one test.
+ return 'both';
+}
+
@customElement('gr-context-controls')
export class GrContextControls extends LitElement {
@property({type: Object}) renderPreferences?: RenderPreferences;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index cf76b8c..92396b0 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -12,7 +12,8 @@
import {GrDiffBuilderImage} from './gr-diff-builder-image';
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
-import {CancelablePromise, makeCancelable} from '../../../scripts/util';
+import {GrDiffBuilderLit} from './gr-diff-builder-lit';
+import {CancelablePromise, makeCancelable} from '../../../utils/async-util';
import {BlameInfo, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {CoverageRange, DiffLayer} from '../../../types/types';
@@ -424,13 +425,24 @@
// If the diff is binary, but not an image.
return new GrDiffBuilderBinary(this.diff, localPrefs, this.diffElement);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
- builder = new GrDiffBuilderSideBySide(
- this.diff,
- localPrefs,
- this.diffElement,
- this.layersInternal,
- this.renderPrefs
- );
+ const useLit = this.renderPrefs?.use_lit_components;
+ if (useLit) {
+ builder = new GrDiffBuilderLit(
+ this.diff,
+ localPrefs,
+ this.diffElement,
+ this.layersInternal,
+ this.renderPrefs
+ );
+ } else {
+ builder = new GrDiffBuilderSideBySide(
+ this.diff,
+ localPrefs,
+ this.diffElement,
+ this.layersInternal,
+ this.renderPrefs
+ );
+ }
} else if (this.viewMode === DiffViewMode.UNIFIED) {
builder = new GrDiffBuilderUnified(
this.diff,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
index 3cdd1f9..75ee088 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
@@ -150,7 +150,7 @@
}
private _setLabelText(label: HTMLElement, image: ImageInfo | null) {
- label.textContent = _getImageLabel(image);
+ label.textContent = getImageLabel(image);
}
private _emitImageLabels(section: HTMLElement) {
@@ -217,7 +217,7 @@
}
}
-function _getImageLabel(image: ImageInfo | null) {
+function getImageLabel(image: ImageInfo | null) {
if (image) {
const type = image.type ?? image._expectedType;
if (image._width && image._height) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
new file mode 100644
index 0000000..054311f
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
@@ -0,0 +1,182 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {RenderPreferences} from '../../../api/diff';
+import {LineNumber} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group';
+import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
+import {Side} from '../../../constants/constants';
+import {DiffLayer, isDefined} from '../../../types/types';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+import {GrDiffBuilder} from './gr-diff-builder';
+import {BlameInfo} from '../../../types/common';
+import {html, render} from 'lit';
+import {GrDiffSection} from './gr-diff-section';
+import '../gr-context-controls/gr-context-controls';
+import './gr-diff-section';
+import {GrDiffRow} from './gr-diff-row';
+
+/**
+ * Base class for builders that are creating the diff using Lit elements.
+ */
+export class GrDiffBuilderLit extends GrDiffBuilder {
+ constructor(
+ diff: DiffInfo,
+ prefs: DiffPreferencesInfo,
+ outputEl: HTMLElement,
+ layers: DiffLayer[] = [],
+ renderPrefs?: RenderPreferences
+ ) {
+ super(diff, prefs, outputEl, layers, renderPrefs);
+ }
+
+ override getContentTdByLine(
+ lineNumber: LineNumber,
+ side?: Side,
+ _root: Element = this.outputEl
+ ): HTMLTableCellElement | null {
+ if (!side) return null;
+ const row = this.findRow(lineNumber, side);
+ return row?.getContentCell(side) ?? null;
+ }
+
+ override getLineElByNumber(lineNumber: LineNumber, side: Side) {
+ const row = this.findRow(lineNumber, side);
+ return row?.getLineNumberCell(side) ?? null;
+ }
+
+ private findRow(lineNumber?: LineNumber, side?: Side): GrDiffRow | undefined {
+ if (!side || !lineNumber) return undefined;
+ const group = this.findGroup(side, lineNumber);
+ if (!group) return undefined;
+ const section = this.findSection(group);
+ if (!section) return undefined;
+ return section.findRow(side, lineNumber);
+ }
+
+ private getDiffRows() {
+ const sections = [
+ ...this.outputEl.querySelectorAll<GrDiffSection>('gr-diff-section'),
+ ];
+ return sections.map(s => s.getDiffRows()).flat();
+ }
+
+ override getLineNumberRows(): HTMLTableRowElement[] {
+ const rows = this.getDiffRows();
+ return rows.map(r => r.getTableRow()).filter(isDefined);
+ }
+
+ override getLineNumEls(side: Side): HTMLTableCellElement[] {
+ const rows = this.getDiffRows();
+ return rows.map(r => r.getLineNumberCell(side)).filter(isDefined);
+ }
+
+ override getBlameTdByLine(lineNumber: number): Element | undefined {
+ return this.findRow(lineNumber, Side.LEFT)?.getBlameCell();
+ }
+
+ override getContentByLine(
+ lineNumber: LineNumber,
+ side?: Side,
+ _root?: HTMLElement
+ ): HTMLElement | null {
+ const cell = this.getContentTdByLine(lineNumber, side);
+ return (cell?.firstChild ?? null) as HTMLElement | null;
+ }
+
+ /** This is used when layers initiate an update. */
+ override renderContentByRange(
+ start: LineNumber,
+ end: LineNumber,
+ side: Side
+ ) {
+ const groups = this.getGroupsByLineRange(start, end, side);
+ for (const group of groups) {
+ const section = this.findSection(group);
+ for (const row of section?.getDiffRows() ?? []) {
+ row.requestUpdate();
+ }
+ }
+ }
+
+ private findSection(group?: GrDiffGroup): GrDiffSection | undefined {
+ if (!group) return undefined;
+ const leftClass = `left-${group.lineRange.left.start_line}`;
+ const rightClass = `right-${group.lineRange.right.start_line}`;
+ return (
+ this.outputEl.querySelector<GrDiffSection>(
+ `gr-diff-section.${leftClass}.${rightClass}`
+ ) ?? undefined
+ );
+ }
+
+ override renderBlameByRange(
+ blameInfo: BlameInfo,
+ start: number,
+ end: number
+ ) {
+ for (let lineNumber = start; lineNumber <= end; lineNumber++) {
+ const row = this.findRow(lineNumber, Side.LEFT);
+ if (!row) continue;
+ row.blameInfo = blameInfo;
+ }
+ }
+
+ // TODO: Refactor this such that adding the move controls becomes part of the
+ // lit element.
+ protected override getMoveControlsConfig() {
+ return {
+ numberOfCells: 4, // How many cells does the diff table have?
+ movedOutIndex: 1, // Index of left content column in diff table.
+ movedInIndex: 3, // Index of right content column in diff table.
+ lineNumberCols: [0, 2], // Indices of line number columns in diff table.
+ };
+ }
+
+ protected override buildSectionElement(group: GrDiffGroup) {
+ const leftCl = `left-${group.lineRange.left.start_line}`;
+ const rightCl = `right-${group.lineRange.right.start_line}`;
+ const section = html`
+ <gr-diff-section
+ class="${leftCl} ${rightCl}"
+ .group=${group}
+ .diff=${this._diff}
+ .layers=${this.layers}
+ .diffPrefs=${this._prefs}
+ .renderPrefs=${this.renderPrefs}
+ ></gr-diff-section>
+ `;
+ // TODO: Refactor GrDiffBuilder.emitGroup() and buildSectionElement()
+ // such that we can render directly into the correct container.
+ const tempContainer = document.createElement('div');
+ render(section, tempContainer);
+ return tempContainer.firstElementChild as GrDiffSection;
+ }
+
+ override addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
+ render(
+ html`
+ <colgroup>
+ <col class=${diffClasses('blame')}></col>
+ <col class=${diffClasses(Side.LEFT)} width=${lineNumberWidth}></col>
+ <col class=${diffClasses(Side.LEFT)}></col>
+ <col class=${diffClasses(Side.RIGHT)} width=${lineNumberWidth}></col>
+ <col class=${diffClasses(Side.RIGHT)}></col>
+ </colgroup>
+ `,
+ outputEl
+ );
+ }
+
+ protected override getNextContentOnSide(
+ _content: HTMLElement,
+ _side: Side
+ ): HTMLElement | null {
+ // TODO: getNextContentOnSide() is not required by lit based rendering.
+ // So let's refactor it to be moved into gr-diff-builder-legacy.
+ console.warn('unimplemented method getNextContentOnSide() called');
+ return null;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
new file mode 100644
index 0000000..4783042
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -0,0 +1,405 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {html, LitElement, TemplateResult} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {ifDefined} from 'lit/directives/if-defined.js';
+import {createRef, Ref, ref} from 'lit/directives/ref.js';
+import {
+ DiffResponsiveMode,
+ Side,
+ LineNumber,
+ DiffLayer,
+} from '../../../api/diff';
+import {BlameInfo} from '../../../types/common';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fire} from '../../../utils/event-util';
+import {getBaseUrl} from '../../../utils/url-util';
+import './gr-diff-text';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {diffClasses, isResponsive} from '../gr-diff/gr-diff-utils';
+
+@customElement('gr-diff-row')
+export class GrDiffRow extends LitElement {
+ contentLeftRef: Ref<LitElement> = createRef();
+
+ contentRightRef: Ref<LitElement> = createRef();
+
+ lineNumberLeftRef: Ref<HTMLTableCellElement> = createRef();
+
+ lineNumberRightRef: Ref<HTMLTableCellElement> = createRef();
+
+ blameCellRef: Ref<HTMLTableCellElement> = createRef();
+
+ tableRowRef: Ref<HTMLTableRowElement> = createRef();
+
+ @property({type: Object})
+ left?: GrDiffLine;
+
+ @property({type: Object})
+ right?: GrDiffLine;
+
+ @property({type: Object})
+ blameInfo?: BlameInfo;
+
+ @property({type: Object})
+ responsiveMode?: DiffResponsiveMode;
+
+ @property({type: Number})
+ tabSize = 2;
+
+ @property({type: Number})
+ lineLength = 80;
+
+ @property({type: Boolean})
+ hideFileCommentButton = false;
+
+ @property({type: Object})
+ layers: DiffLayer[] = [];
+
+ /**
+ * While not visible we are trying to optimize rendering performance by
+ * rendering a simpler version of the diff. Once this has become true it
+ * cannot be set back to false.
+ */
+ @state()
+ isVisible = false;
+
+ /**
+ * Semantic DOM diff testing does not work with just table fragments, so when
+ * running such tests the render() method has to wrap the DOM in a proper
+ * <table> element.
+ */
+ @state()
+ addTableWrapperForTesting = false;
+
+ /**
+ * Keeps track of whether diff layers have already been applied to the diff
+ * row. That happens after the DOM has been created in the `updated()`
+ * lifecycle callback.
+ *
+ * Once layers are applied, the diff row requires two rendering passes for an
+ * update: 1. Remove all <gr-diff-text> elements and their layer manipulated
+ * DOMs. 2. Add fresh <gr-diff-text> elements and let layers re-apply in
+ * `updated()`.
+ */
+ private layersApplied = false;
+
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ override updated() {
+ if (this.layersApplied) {
+ // <gr-diff-text> elements have been removed during rendering. Let's start
+ // another rendering cycle with freshly created <gr-diff-text> elements.
+ this.updateComplete.then(() => {
+ this.layersApplied = false;
+ this.requestUpdate();
+ });
+ } else {
+ this.updateLayers(Side.LEFT);
+ this.updateLayers(Side.RIGHT);
+ }
+ }
+
+ /**
+ * The diff layers API is designed to let layers manipulate the DOM. So we
+ * have to apply them after the rendering cycle is done (`updated()`). But
+ * when re-rendering a row that already has layers applied, then we have to
+ * first wipe away <gr-diff-text>. This is achieved by
+ * `this.layersApplied = true`.
+ */
+ private async updateLayers(side: Side) {
+ if (!this.isVisible) return;
+ const line = this.line(side);
+ const contentEl = this.contentRef(side).value;
+ const lineNumberEl = this.lineNumberRef(side).value;
+ if (!line || !contentEl || !lineNumberEl) return;
+
+ // We have to wait for the <gr-diff-text> child component to finish
+ // rendering before we can apply layers, which will re-write the HTML.
+ await contentEl?.updateComplete;
+ for (const layer of this.layers) {
+ if (typeof layer.annotate === 'function') {
+ layer.annotate(contentEl, lineNumberEl, line, side);
+ }
+ }
+ // At this point we consider layers applied. So as soon as <gr-diff-row>
+ // enters a new rendering cycle <gr-diff-text> elements will be removed.
+ this.layersApplied = true;
+ }
+
+ private renderInvisible() {
+ return html`
+ <tr>
+ <td class="gr-diff blame"></td>
+ <td class="gr-diff left"></td>
+ <td class="gr-diff left content">
+ <div>${this.left?.text ?? ''}</div>
+ </td>
+ <td class="gr-diff right"></td>
+ <td class="gr-diff right content">
+ <div>${this.right?.text ?? ''}</div>
+ </td>
+ </tr>
+ `;
+ }
+
+ override render() {
+ if (!this.left || !this.right) return;
+ if (!this.isVisible) return this.renderInvisible();
+ const row = html`
+ <tr
+ ${ref(this.tableRowRef)}
+ class=${diffClasses('diff-row', 'side-by-side')}
+ left-type=${this.left.type}
+ right-type=${this.right.type}
+ tabindex="-1"
+ >
+ ${this.renderBlameCell()} ${this.renderLineNumberCell(Side.LEFT)}
+ ${this.renderContentCell(Side.LEFT)}
+ ${this.renderLineNumberCell(Side.RIGHT)}
+ ${this.renderContentCell(Side.RIGHT)}
+ </tr>
+ `;
+ if (this.addTableWrapperForTesting) {
+ return html`<table>
+ ${row}
+ </table>`;
+ }
+ return row;
+ }
+
+ getTableRow(): HTMLTableRowElement | undefined {
+ return this.tableRowRef.value;
+ }
+
+ getLineNumberCell(side: Side): HTMLTableCellElement | undefined {
+ return this.lineNumberRef(side).value;
+ }
+
+ getContentCell(side: Side) {
+ const div = this.contentRef(side)?.value;
+ if (!div) return undefined;
+ return div.parentElement as HTMLTableCellElement;
+ }
+
+ getBlameCell() {
+ return this.blameCellRef.value;
+ }
+
+ private renderBlameCell() {
+ // td.blame has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`
+ <td
+ ${ref(this.blameCellRef)}
+ class=${diffClasses('blame')}
+ data-line-number=${this.left?.beforeNumber ?? 0}
+ >${this.renderBlameElement()}</td>
+ `;
+ }
+
+ private renderBlameElement() {
+ const lineNum = this.left?.beforeNumber;
+ const commit = this.blameInfo;
+ if (!lineNum || !commit) return;
+
+ const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
+ const extras: string[] = [];
+ if (isStartOfRange) extras.push('startOfRange');
+ const date = new Date(commit.time * 1000).toLocaleDateString();
+ const shortName = commit.author.split(' ')[0];
+ const url = `${getBaseUrl()}/q/${commit.id}`;
+
+ // td.blame has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`<span class=${diffClasses(...extras)}
+ ><a href=${url} class=${diffClasses('blameDate')}>${date}</a
+ ><span class=${diffClasses('blameAuthor')}> ${shortName}</span
+ ><gr-hovercard class=${diffClasses()}>
+ <span class=${diffClasses('blameHoverCard')}>
+ Commit ${commit.id}<br />
+ Author: ${commit.author}<br />
+ Date: ${date}<br />
+ <br />
+ ${commit.commit_msg}
+ </span>
+ </gr-hovercard
+ ></span>`;
+ }
+
+ private renderLineNumberCell(side: Side): TemplateResult {
+ const line = this.line(side);
+ const lineNumber = this.lineNumber(side);
+ if (
+ !line ||
+ !lineNumber ||
+ line.type === GrDiffLineType.BLANK ||
+ this.layersApplied
+ ) {
+ return html`<td
+ ${ref(this.lineNumberRef(side))}
+ class=${diffClasses(side)}
+ ></td>`;
+ }
+
+ return html`<td
+ ${ref(this.lineNumberRef(side))}
+ class=${diffClasses(side, 'lineNum')}
+ data-value=${lineNumber}
+ >
+ ${this.renderLineNumberButton(line, lineNumber, side)}
+ </td>`;
+ }
+
+ private renderLineNumberButton(
+ line: GrDiffLine,
+ lineNumber: LineNumber,
+ side: Side
+ ) {
+ if (this.hideFileCommentButton && lineNumber === 'FILE') return;
+ if (lineNumber === 'LOST') return;
+ // .lineNumButton has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`
+ <button
+ class=${diffClasses('lineNumButton', side)}
+ tabindex="-1"
+ data-value=${lineNumber}
+ aria-label=${ifDefined(
+ this.computeLineNumberAriaLabel(line, lineNumber)
+ )}
+ @mouseenter=${() =>
+ fire(this, 'line-mouse-enter', {lineNum: lineNumber, side})}
+ @mouseleave=${() =>
+ fire(this, 'line-mouse-leave', {lineNum: lineNumber, side})}
+ >${lineNumber === 'FILE' ? 'File' : lineNumber.toString()}</button>
+ `;
+ }
+
+ private computeLineNumberAriaLabel(line: GrDiffLine, lineNumber: LineNumber) {
+ if (lineNumber === 'FILE') return 'Add file comment';
+
+ // Add aria-labels for valid line numbers.
+ // For unified diff, this method will be called with number set to 0 for
+ // the empty line number column for added/removed lines. This should not
+ // be announced to the screenreader.
+ if (lineNumber <= 0) return undefined;
+
+ switch (line.type) {
+ case GrDiffLineType.REMOVE:
+ return `${lineNumber} removed`;
+ case GrDiffLineType.ADD:
+ return `${lineNumber} added`;
+ case GrDiffLineType.BOTH:
+ case GrDiffLineType.BLANK:
+ return undefined;
+ }
+ }
+
+ private renderContentCell(side: Side): TemplateResult {
+ const line = this.line(side);
+ const lineNumber = this.lineNumber(side);
+ assertIsDefined(line, 'line');
+ const extras: string[] = [line.type, side];
+ if (line.type !== GrDiffLineType.BLANK) extras.push('content');
+ if (!line.hasIntralineInfo) extras.push('no-intraline-info');
+ if (line.beforeNumber === 'FILE') extras.push('file');
+ if (line.beforeNumber === 'LOST') extras.push('lost');
+
+ // .content has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`
+ <td
+ class=${diffClasses(...extras)}
+ @mouseenter=${() => {
+ if (lineNumber)
+ fire(this, 'line-mouse-enter', {lineNum: lineNumber, side});
+ }}
+ @mouseleave=${() => {
+ if (lineNumber)
+ fire(this, 'line-mouse-leave', {lineNum: lineNumber, side});
+ }}
+ >${this.renderText(side)}${this.renderThreadGroup(side, lineNumber)}</td>
+ `;
+ }
+
+ private renderThreadGroup(side: Side, lineNumber?: LineNumber) {
+ if (!lineNumber) return;
+ // TODO: For the LOST line number the convention is that a <tr> will always
+ // be rendered, but it will not be visible, because of all cells being
+ // empty. For this to work with lit-based rendering we may only render a
+ // thread-group and a <slot> when there is a thread using that slot. The
+ // cleanest solution for that is probably introducing a gr-diff-model, where
+ // each diff row can look up or observe comment threads.
+ // .content has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`<div class="thread-group" data-side=${side}><slot name="${side}-${lineNumber}"></slot></div>`;
+ }
+
+ private contentRef(side: Side) {
+ return side === Side.LEFT ? this.contentLeftRef : this.contentRightRef;
+ }
+
+ private lineNumberRef(side: Side) {
+ return side === Side.LEFT
+ ? this.lineNumberLeftRef
+ : this.lineNumberRightRef;
+ }
+
+ private lineNumber(side: Side) {
+ return this.line(side)?.lineNumber(side);
+ }
+
+ private line(side: Side) {
+ return side === Side.LEFT ? this.left : this.right;
+ }
+
+ /**
+ * Returns a 'div' element containing the supplied |text| as its innerText,
+ * with '\t' characters expanded to a width determined by |tabSize|, and the
+ * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
+ * desired.
+ */
+ private renderText(side: Side) {
+ const line = this.line(side);
+ const lineNumber = this.lineNumber(side);
+ if (lineNumber === 'FILE' || lineNumber === 'LOST') return;
+
+ // Note that `this.layersApplied` will wipe away the <gr-diff-text>, and
+ // another rendering cycle will be initiated in `updated()`.
+ // prettier-ignore
+ const textElement = line?.text && !this.layersApplied
+ ? html`<gr-diff-text
+ ${ref(this.contentRef(side))}
+ .text=${line?.text}
+ .tabSize=${this.tabSize}
+ .lineLimit=${this.lineLength}
+ .isResponsive=${isResponsive(this.responsiveMode)}
+ ></gr-diff-text>` : '';
+ // .content has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`<div
+ class=${diffClasses('contentText', side)}
+ .ariaLabel=${line?.text ?? ''}
+ data-side=${ifDefined(side)}
+ >${textElement}</div>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-diff-row': GrDiffRow;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
new file mode 100644
index 0000000..4e8bb62
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
@@ -0,0 +1,199 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-diff-row';
+import {GrDiffRow} from './gr-diff-row';
+import {fixture, html, assert} from '@open-wc/testing';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+import {GrDiffLineType} from '../../../api/diff';
+
+suite('gr-diff-row test', () => {
+ let element: GrDiffRow;
+
+ setup(async () => {
+ element = await fixture<GrDiffRow>(html`<gr-diff-row></gr-diff-row>`);
+ element.isVisible = true;
+ element.addTableWrapperForTesting = true;
+ await element.updateComplete;
+ });
+
+ test('both', async () => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH, 1, 1);
+ line.text = 'lorem ipsum';
+ element.left = line;
+ element.right = line;
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="1"></td>
+ <td class="gr-diff left lineNum" data-value="1">
+ <button
+ class="gr-diff left lineNumButton"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff left"
+ data-side="left"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="1">
+ <button
+ class="gr-diff lineNumButton right"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff right"
+ data-side="right"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+ });
+
+ test('add', async () => {
+ const line = new GrDiffLine(GrDiffLineType.ADD, 0, 1);
+ line.text = 'lorem ipsum';
+ element.left = new GrDiffLine(GrDiffLineType.BLANK);
+ element.right = line;
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ aria-label=""
+ class="contentText gr-diff left"
+ data-side="left"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="1">
+ <button
+ aria-label="1 added"
+ class="gr-diff lineNumButton right"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff right"
+ data-side="right"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+ });
+
+ test('remove', async () => {
+ const line = new GrDiffLine(GrDiffLineType.REMOVE, 1, 0);
+ line.text = 'lorem ipsum';
+ element.left = line;
+ element.right = new GrDiffLine(GrDiffLineType.BLANK);
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="blank"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="1"></td>
+ <td class="gr-diff left lineNum" data-value="1">
+ <button
+ aria-label="1 removed"
+ class="gr-diff left lineNumButton"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff left"
+ data-side="left"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff right"></td>
+ <td class="blank gr-diff no-intraline-info right">
+ <div
+ aria-label=""
+ class="contentText gr-diff right"
+ data-side="right"
+ ></div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+ });
+});
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
new file mode 100644
index 0000000..a1cde67
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -0,0 +1,240 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {
+ DiffInfo,
+ DiffLayer,
+ DiffViewMode,
+ MovedLinkClickedEventDetail,
+ RenderPreferences,
+ Side,
+ LineNumber,
+ DiffPreferencesInfo,
+} from '../../../api/diff';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {countLines, diffClasses} 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';
+import '../gr-range-header/gr-range-header';
+import './gr-diff-row';
+import {whenVisible} from '../../../utils/dom-util';
+
+@customElement('gr-diff-section')
+export class GrDiffSection extends LitElement {
+ @property({type: Object})
+ group?: GrDiffGroup;
+
+ @property({type: Object})
+ diff?: DiffInfo;
+
+ @property({type: Object})
+ renderPrefs?: RenderPreferences;
+
+ @property({type: Object})
+ diffPrefs?: DiffPreferencesInfo;
+
+ @property({type: Object})
+ layers: DiffLayer[] = [];
+
+ /**
+ * While not visible we are trying to optimize rendering performance by
+ * rendering a simpler version of the diff.
+ */
+ @state()
+ isVisible = false;
+
+ /**
+ * Semantic DOM diff testing does not work with just table fragments, so when
+ * running such tests the render() method has to wrap the DOM in a proper
+ * <table> element.
+ */
+ @state()
+ addTableWrapperForTesting = false;
+
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ // TODO: Refine this obviously simplistic approach to optimized rendering.
+ whenVisible(this.parentElement!, () => (this.isVisible = true), 1000);
+ }
+
+ override render() {
+ if (!this.group) return;
+ const extras: string[] = [];
+ extras.push('section');
+ extras.push(this.group.type);
+ if (this.group.isTotal()) extras.push('total');
+ if (this.group.dueToRebase) extras.push('dueToRebase');
+ if (this.group.moveDetails) extras.push('dueToMove');
+ if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly');
+
+ const isControl = this.group.type === GrDiffGroupType.CONTEXT_CONTROL;
+ const pairs = isControl ? [] : this.group.getSideBySidePairs();
+ const body = html`
+ <tbody class=${diffClasses(...extras)}>
+ ${this.renderContextControls()} ${this.renderMoveControls()}
+ ${pairs.map(pair => {
+ const leftCl = `left-${pair.left.lineNumber(Side.LEFT)}`;
+ const rightCl = `right-${pair.right.lineNumber(Side.RIGHT)}`;
+ return html`
+ <gr-diff-row
+ class="${leftCl} ${rightCl}"
+ .left=${pair.left}
+ .right=${pair.right}
+ .layers=${this.layers}
+ .lineLength=${this.diffPrefs?.line_length ?? 80}
+ .tabSize=${this.diffPrefs?.tab_size ?? 2}
+ .isVisible=${this.isVisible}
+ >
+ </gr-diff-row>
+ `;
+ })}
+ </tbody>
+ `;
+ if (this.addTableWrapperForTesting) {
+ return html`<table>
+ ${body}
+ </table>`;
+ }
+ return body;
+ }
+
+ getDiffRows(): GrDiffRow[] {
+ return [...this.querySelectorAll<GrDiffRow>('gr-diff-row')];
+ }
+
+ private renderContextControls() {
+ if (this.group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
+
+ const leftStart = this.group.lineRange.left.start_line;
+ const leftEnd = this.group.lineRange.left.end_line;
+ const firstGroupIsSkipped = !!this.group.contextGroups[0].skip;
+ const lastGroupIsSkipped =
+ !!this.group.contextGroups[this.group.contextGroups.length - 1].skip;
+ const lineCountLeft = countLines(this.diff, Side.LEFT);
+ const containsWholeFile = lineCountLeft === leftEnd - leftStart + 1;
+ const showAbove =
+ (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
+ const showBelow = leftEnd < lineCountLeft && !lastGroupIsSkipped;
+
+ return html`
+ <gr-context-controls-section
+ .showAbove=${showAbove}
+ .showBelow=${showBelow}
+ .group=${this.group}
+ .diff=${this.diff}
+ .renderPrefs=${this.renderPrefs}
+ .viewMode=${DiffViewMode.SIDE_BY_SIDE}
+ >
+ </gr-context-controls-section>
+ `;
+ }
+
+ findRow(side: Side, lineNumber: LineNumber): GrDiffRow | undefined {
+ return (
+ this.querySelector<GrDiffRow>(`gr-diff-row.${side}-${lineNumber}`) ??
+ undefined
+ );
+ }
+
+ private renderMoveControls() {
+ if (!this.group?.moveDetails) return;
+ const movedIn = this.group.adds.length > 0;
+ const plainCell = html`<td class=${diffClasses()}></td>`;
+ const lineNumberCell = html`
+ <td class=${diffClasses('moveControlsLineNumCol')}></td>
+ `;
+ const moveCell = html`
+ <td class=${diffClasses('moveHeader')}>
+ <gr-range-header class=${diffClasses()} icon="gr-icons:move-item">
+ ${this.renderMoveDescription(movedIn)}
+ </gr-range-header>
+ </td>
+ `;
+ return html`
+ <tr
+ class=${diffClasses('moveControls', movedIn ? 'movedIn' : 'movedOut')}
+ >
+ ${lineNumberCell} ${movedIn ? plainCell : moveCell} ${lineNumberCell}
+ ${movedIn ? moveCell : plainCell}
+ </tr>
+ `;
+ }
+
+ private renderMoveDescription(movedIn: boolean) {
+ if (this.group?.moveDetails?.range) {
+ const {changed, range} = this.group.moveDetails;
+ const otherSide = movedIn ? Side.LEFT : Side.RIGHT;
+ const andChangedLabel = changed ? 'and changed ' : '';
+ const direction = movedIn ? 'from' : 'to';
+ const textLabel = `Moved ${andChangedLabel}${direction} lines `;
+ return html`
+ <div class=${diffClasses()}>
+ <span class=${diffClasses()}>${textLabel}</span>
+ ${this.renderMovedLineAnchor(range.start, otherSide)}
+ <span class=${diffClasses()}> - </span>
+ ${this.renderMovedLineAnchor(range.end, otherSide)}
+ </div>
+ `;
+ }
+
+ return html`
+ <div class=${diffClasses()}>
+ <span class=${diffClasses()}
+ >${movedIn ? 'Moved in' : 'Moved out'}</span
+ >
+ </div>
+ `;
+ }
+
+ private renderMovedLineAnchor(line: number, side: Side) {
+ const listener = (e: MouseEvent) => {
+ e.preventDefault();
+ this.handleMovedLineAnchorClick(e.target, side, line);
+ };
+ // `href` is not actually used but important for Screen Readers
+ return html`
+ <a class=${diffClasses()} href=${`#${line}`} @click=${listener}
+ >${line}</a
+ >
+ `;
+ }
+
+ private handleMovedLineAnchorClick(
+ anchor: EventTarget | null,
+ side: Side,
+ line: number
+ ) {
+ anchor?.dispatchEvent(
+ new CustomEvent<MovedLinkClickedEventDetail>('moved-link-clicked', {
+ detail: {
+ lineNum: line,
+ side,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-diff-section': GrDiffSection;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
new file mode 100644
index 0000000..363b001
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
@@ -0,0 +1,201 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-diff-section';
+import {GrDiffSection} from './gr-diff-section';
+import {fixture, html, assert} from '@open-wc/testing';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+import {GrDiffLineType} from '../../../api/diff';
+
+suite('gr-diff-section test', () => {
+ let element: GrDiffSection;
+
+ setup(async () => {
+ element = await fixture<GrDiffSection>(
+ html`<gr-diff-section></gr-diff-section>`
+ );
+ element.addTableWrapperForTesting = true;
+ element.isVisible = true;
+ await element.updateComplete;
+ });
+
+ test('3 normal unchanged rows', async () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
+ ];
+ lines[0].text = 'asdf';
+ lines[1].text = 'qwer';
+ lines[2].text = 'zxcv';
+ const group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
+ element.group = group;
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+ <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+ <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+ <table>
+ <tbody class="both gr-diff section">
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="1"></td>
+ <td class="gr-diff left lineNum" data-value="1">
+ <button
+ class="gr-diff left lineNumButton"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ aria-label="asdf"
+ class="contentText gr-diff left"
+ data-side="left"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="1">
+ <button
+ class="gr-diff lineNumButton right"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ aria-label="asdf"
+ class="contentText gr-diff right"
+ data-side="right"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="1"></td>
+ <td class="gr-diff left lineNum" data-value="1">
+ <button
+ class="gr-diff left lineNumButton"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ aria-label="qwer"
+ class="contentText gr-diff left"
+ data-side="left"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="1">
+ <button
+ class="gr-diff lineNumButton right"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ aria-label="qwer"
+ class="contentText gr-diff right"
+ data-side="right"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="1"></td>
+ <td class="gr-diff left lineNum" data-value="1">
+ <button
+ class="gr-diff left lineNumButton"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ aria-label="zxcv"
+ class="contentText gr-diff left"
+ data-side="left"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="1">
+ <button
+ class="gr-diff lineNumButton right"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ aria-label="zxcv"
+ class="contentText gr-diff right"
+ data-side="right"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+ });
+});
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
new file mode 100644
index 0000000..d5f0e1b
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
@@ -0,0 +1,151 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {LitElement, html, TemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+
+const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+const TAB = '\t';
+
+/**
+ * Renders one line of code on one side of the diff. It takes care of:
+ * - Tabs, see `tabSize` property.
+ * - Line Breaks, see `lineLimit` property.
+ * - Surrogate Character Pairs.
+ *
+ * Note that other modifications to the code in a gr-diff is done via diff
+ * layers, which manipulate the DOM directly. So `gr-diff-text` is thrown
+ * away and re-rendered every time something changes by its parent
+ * `gr-diff-row`. So don't bother to optimize this component for re-rendering
+ * performance. And be aware that building longer lived local state is not
+ * useful here.
+ */
+@customElement('gr-diff-text')
+export class GrDiffText extends LitElement {
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ @property({type: String})
+ text = '';
+
+ @property({type: Boolean})
+ isResponsive = false;
+
+ @property({type: Number})
+ tabSize = 2;
+
+ @property({type: Number})
+ lineLimit = 80;
+
+ /** Temporary state while rendering. */
+ private textOffset = 0;
+
+ /** Temporary state while rendering. */
+ private columnPos = 0;
+
+ /** Temporary state while rendering. */
+ private pieces: (string | TemplateResult)[] = [];
+
+ /** Split up the string into tabs, surrogate pairs and regular segments. */
+ override render() {
+ this.textOffset = 0;
+ this.columnPos = 0;
+ this.pieces = [];
+ const splitByTab = this.text.split('\t');
+ for (let i = 0; i < splitByTab.length; i++) {
+ const splitBySurrogate = splitByTab[i].split(SURROGATE_PAIR);
+ for (let j = 0; j < splitBySurrogate.length; j++) {
+ this.renderSegment(splitBySurrogate[j]);
+ if (j < splitBySurrogate.length - 1) {
+ this.renderSurrogatePair();
+ }
+ }
+ if (i < splitByTab.length - 1) {
+ this.renderTab();
+ }
+ }
+ if (this.textOffset !== this.text.length) throw new Error('unfinished');
+ return this.pieces;
+ }
+
+ /** Render regular characters, but insert line breaks appropriately. */
+ private renderSegment(segment: string) {
+ let segmentOffset = 0;
+ while (segmentOffset < segment.length) {
+ const newOffset = Math.min(
+ segment.length,
+ segmentOffset + this.lineLimit - this.columnPos
+ );
+ this.renderString(segment.substring(segmentOffset, newOffset));
+ segmentOffset = newOffset;
+ if (segmentOffset < segment.length && this.columnPos === this.lineLimit) {
+ this.renderLineBreak();
+ }
+ }
+ }
+
+ /** Render regular characters. */
+ private renderString(s: string) {
+ if (s.length === 0) return;
+ this.pieces.push(s);
+ this.textOffset += s.length;
+ this.columnPos += s.length;
+ if (this.columnPos > this.lineLimit) throw new Error('over line limit');
+ }
+
+ /** Render a tab character. */
+ private renderTab() {
+ let tabSize = this.tabSize - (this.columnPos % this.tabSize);
+ if (this.columnPos + tabSize > this.lineLimit) {
+ this.renderLineBreak();
+ tabSize = this.tabSize;
+ }
+ const piece = html`<span
+ class=${diffClasses('tab')}
+ style="tab-size: ${tabSize}; -moz-tab-size: ${tabSize};"
+ >${TAB}</span
+ >`;
+ this.pieces.push(piece);
+ this.textOffset += 1;
+ this.columnPos += tabSize;
+ }
+
+ /** Render a surrogate pair: string length is 2, but is just 1 char. */
+ private renderSurrogatePair() {
+ if (this.columnPos === this.lineLimit) {
+ this.renderLineBreak();
+ }
+ this.pieces.push(this.text.substring(this.textOffset, this.textOffset + 2));
+ this.textOffset += 2;
+ this.columnPos += 1;
+ }
+
+ /** Render a line break, don't advance text offset, reset col position. */
+ private renderLineBreak() {
+ if (this.isResponsive) {
+ this.pieces.push(html`<wbr class=${diffClasses()}></wbr>`);
+ } else {
+ this.pieces.push(html`<span class=${diffClasses('br')}></span>`);
+ }
+ // this.textOffset += 0;
+ this.columnPos = 0;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-diff-text': GrDiffText;
+ }
+}
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
new file mode 100644
index 0000000..aec7fe0
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -0,0 +1,153 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-diff-text';
+import {GrDiffText} from './gr-diff-text';
+import {fixture, html, assert} from '@open-wc/testing';
+
+const LINE_BREAK = '<span class="gr-diff br"></span>';
+
+const TAB = '<span class="" style=""></span>';
+
+const TAB_IGNORE = ['class', 'style'];
+
+suite('gr-diff-text test', () => {
+ let element: GrDiffText;
+
+ setup(async () => {
+ element = await fixture<GrDiffText>(
+ html`<gr-diff-text tabsize="4" linelimit="10"></gr-diff-text>`
+ );
+ });
+
+ const check = async (
+ text: string,
+ html: string,
+ ignoreAttributes: string[] = []
+ ) => {
+ element.text = text;
+ await element.updateComplete;
+ assert.lightDom.equal(element, html, {ignoreAttributes});
+ };
+
+ suite('lit rendering', () => {
+ test('renderText newlines 1', async () => {
+ await check('abcdef', 'abcdef');
+ await check('a'.repeat(20), `aaaaaaaaaa${LINE_BREAK}aaaaaaaaaa`);
+ });
+
+ test('renderText newlines 2', async () => {
+ await check(
+ '<span class="thumbsup">👍</span>',
+ '<span clas' +
+ LINE_BREAK +
+ 's="thumbsu' +
+ LINE_BREAK +
+ 'p">👍</span' +
+ LINE_BREAK +
+ '>'
+ );
+ });
+
+ test('renderText newlines 3', async () => {
+ await check(
+ '01234\t56789',
+ '01234' + TAB + '56' + LINE_BREAK + '789',
+ TAB_IGNORE
+ );
+ });
+
+ test('renderText newlines 4', async () => {
+ element.lineLimit = 20;
+ await element.updateComplete;
+ await check(
+ '👍'.repeat(58),
+ '👍'.repeat(20) +
+ LINE_BREAK +
+ '👍'.repeat(20) +
+ LINE_BREAK +
+ '👍'.repeat(18)
+ );
+ });
+
+ test('tab wrapper style', async () => {
+ element.lineLimit = 100;
+ 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}; -moz-tab-size: ${size};"
+ >
+ </span>
+ `
+ );
+ }
+ });
+
+ test('tab wrapper insertion', async () => {
+ await check('abc\tdef', 'abc' + TAB + 'def', TAB_IGNORE);
+ });
+
+ test('escaping HTML', async () => {
+ element.lineLimit = 100;
+ await element.updateComplete;
+ await check(
+ '<script>alert("XSS");<' + '/script>',
+ '<script>alert("XSS");</script>'
+ );
+ await check('& < > " \' / `', '& < > " \' / `');
+ });
+
+ test('text length with tabs and unicode', async () => {
+ async function expectTextLength(
+ text: string,
+ tabSize: number,
+ expected: number
+ ) {
+ element.text = text;
+ element.tabSize = tabSize;
+ element.lineLimit = expected;
+ await element.updateComplete;
+ const result = element.innerHTML;
+
+ // Must not contain a line break.
+ assert.isNotOk(element.querySelector('span.br'));
+
+ // Increasing the line limit by 1 should not change anything.
+ element.lineLimit = expected + 1;
+ await element.updateComplete;
+ const resultPlusOne = element.innerHTML;
+ assert.equal(resultPlusOne, result);
+
+ // Increasing the line limit to infinity should not change anything.
+ element.lineLimit = Infinity;
+ await element.updateComplete;
+ const resultInf = element.innerHTML;
+ assert.equal(resultInf, result);
+
+ // Decreasing the line limit by 1 should introduce a line break.
+ element.lineLimit = expected + 1;
+ await element.updateComplete;
+ assert.isNotOk(element.querySelector('span.br'));
+ }
+ expectTextLength('12345', 4, 5);
+ expectTextLength('\t\t12', 4, 10);
+ expectTextLength('abc💢123', 4, 7);
+ expectTextLength('abc\t', 8, 8);
+ expectTextLength('abc\t\t', 10, 20);
+ expectTextLength('', 10, 0);
+ // 17 Thai combining chars.
+ expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
+ expectTextLength('abc\tde', 10, 12);
+ expectTextLength('abc\tde\t', 10, 20);
+ expectTextLength('\t\t\t\t\t', 20, 100);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.js b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
similarity index 71%
rename from polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
index c5cab0c..6c45f20 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
@@ -7,25 +7,24 @@
import '../../../test/common-test-setup';
import {GrAnnotation} from './gr-annotation';
import {
- sanitizeDOMValue,
+ getSanitizeDOMValue,
setSanitizeDOMValue,
} from '@polymer/polymer/lib/utils/settings';
-// eslint-disable-next-line import/named
import {assert, fixture, html} from '@open-wc/testing';
suite('annotation', () => {
- let str;
- let parent;
- let textNode;
+ let str: string;
+ let parent: HTMLDivElement;
+ let textNode: Text;
setup(async () => {
parent = await fixture(
- html`
+ html`
<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
`
);
- textNode = parent.childNodes[0];
- str = textNode.textContent;
+ textNode = parent.childNodes[0] as Text;
+ str = textNode.textContent!;
});
test('_annotateText Case 1', () => {
@@ -33,9 +32,10 @@
assert.equal(parent.childNodes.length, 1);
assert.instanceOf(parent.childNodes[0], HTMLElement);
- assert.equal(parent.childNodes[0].className, 'foobar');
- assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
- assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+ const firstChild = parent.childNodes[0] as HTMLElement;
+ assert.equal(firstChild.className, 'foobar');
+ assert.instanceOf(firstChild.childNodes[0], Text);
+ assert.equal(firstChild.childNodes[0].textContent, str);
});
test('_annotateText Case 2', () => {
@@ -48,9 +48,10 @@
assert.equal(parent.childNodes.length, 2);
assert.instanceOf(parent.childNodes[0], HTMLElement);
- assert.equal(parent.childNodes[0].className, 'foobar');
- assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
- assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
+ const firstChild = parent.childNodes[0] as HTMLElement;
+ assert.equal(firstChild.className, 'foobar');
+ assert.instanceOf(firstChild.childNodes[0], Text);
+ assert.equal(firstChild.childNodes[0].textContent, substr);
assert.instanceOf(parent.childNodes[1], Text);
assert.equal(parent.childNodes[1].textContent, remainder);
@@ -69,10 +70,11 @@
assert.instanceOf(parent.childNodes[0], Text);
assert.equal(parent.childNodes[0].textContent, remainder);
- assert.instanceOf(parent.childNodes[1], HTMLElement);
- assert.equal(parent.childNodes[1].className, 'foobar');
- assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
- assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ const secondChild = parent.childNodes[1] as HTMLElement;
+ assert.instanceOf(secondChild, HTMLElement);
+ assert.equal(secondChild.className, 'foobar');
+ assert.instanceOf(secondChild.childNodes[0], Text);
+ assert.equal(secondChild.childNodes[0].textContent, substr);
});
test('_annotateText Case 4', () => {
@@ -90,10 +92,11 @@
assert.instanceOf(parent.childNodes[0], Text);
assert.equal(parent.childNodes[0].textContent, remainderPre);
- assert.instanceOf(parent.childNodes[1], HTMLElement);
- assert.equal(parent.childNodes[1].className, 'foobar');
- assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
- assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ const secondChild = parent.childNodes[1] as HTMLElement;
+ assert.instanceOf(secondChild, HTMLElement);
+ assert.equal(secondChild.className, 'foobar');
+ assert.instanceOf(secondChild.childNodes[0], Text);
+ assert.equal(secondChild.childNodes[0].textContent, substr);
assert.instanceOf(parent.childNodes[2], Text);
assert.equal(parent.childNodes[2].textContent, remainderPost);
@@ -105,35 +108,35 @@
// Apply the layers successively.
layers.forEach((layer, i) => {
GrAnnotation.annotateElement(
- parent,
- str.indexOf(layer),
- layer.length,
- `layer-${i + 1}`
+ parent,
+ str.indexOf(layer),
+ layer.length,
+ `layer-${i + 1}`
);
});
assert.equal(parent.textContent, str);
// Layer 1:
- const layer1 = parent.querySelectorAll('.layer-1');
+ const layer1 = parent.querySelectorAll<HTMLElement>('.layer-1');
assert.equal(layer1.length, 1);
assert.equal(layer1[0].textContent, layers[0]);
assert.equal(layer1[0].parentElement, parent);
// Layer 2:
- const layer2 = parent.querySelectorAll('.layer-2');
+ const layer2 = parent.querySelectorAll<HTMLElement>('.layer-2');
assert.equal(layer2.length, 1);
assert.equal(layer2[0].textContent, layers[1]);
assert.equal(layer2[0].parentElement, parent);
// Layer 3:
- const layer3 = parent.querySelectorAll('.layer-3');
+ const layer3 = parent.querySelectorAll<HTMLElement>('.layer-3');
assert.equal(layer3.length, 1);
assert.equal(layer3[0].textContent, layers[2]);
assert.equal(layer3[0].parentElement, layer1[0]);
// Layer 4:
- const layer4 = parent.querySelectorAll('.layer-4');
+ const layer4 = parent.querySelectorAll<HTMLElement>('.layer-4');
assert.equal(layer4.length, 3);
assert.equal(layer4[0].textContent, 'et, ');
@@ -146,8 +149,12 @@
assert.equal(layer4[2].parentElement, layer2[0]);
assert.equal(
- layer4[0].textContent + layer4[1].textContent + layer4[2].textContent,
- layers[3]
+ [
+ layer4[0].textContent,
+ layer4[1].textContent,
+ layer4[2].textContent,
+ ].join(''),
+ layers[3]
);
});
@@ -174,12 +181,17 @@
suite('annotateWithElement', () => {
const fullText = '01234567890123456789';
- let mockSanitize;
- let originalSanitizeDOMValue;
+ let mockSanitize: sinon.SinonSpy;
+ let originalSanitizeDOMValue: (
+ p0: any,
+ p1: string,
+ p2: string,
+ p3: Node | null
+ ) => any;
setup(() => {
- setSanitizeDOMValue((p0, p1, p2, node) => p0);
- originalSanitizeDOMValue = sanitizeDOMValue;
+ setSanitizeDOMValue(p0 => p0);
+ originalSanitizeDOMValue = getSanitizeDOMValue()!;
assert.isDefined(originalSanitizeDOMValue);
mockSanitize = sinon.spy(originalSanitizeDOMValue);
setSanitizeDOMValue(mockSanitize);
@@ -198,8 +210,8 @@
});
assert.equal(
- container.innerHTML,
- '0<test-wrapper>1234567890</test-wrapper>123456789'
+ container.innerHTML,
+ '0<test-wrapper>1234567890</test-wrapper>123456789'
);
});
@@ -213,8 +225,8 @@
});
assert.equal(
- container.innerHTML,
- '0' +
+ container.innerHTML,
+ '0' +
'<test-wrapper>' +
'1234' +
'<hl class="testclass">567890</hl>' +
@@ -233,8 +245,8 @@
});
assert.equal(
- container.innerHTML,
- '0<test-wrapper>1234567890</test-wrapper>123456789'
+ container.innerHTML,
+ '0<test-wrapper>1234567890</test-wrapper>123456789'
);
});
@@ -248,8 +260,8 @@
});
assert.equal(
- container.innerHTML,
- '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789'
+ container.innerHTML,
+ '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789'
);
});
@@ -265,8 +277,8 @@
});
assert.equal(
- container.innerHTML,
- '<!--comment1-->' +
+ container.innerHTML,
+ '<!--comment1-->' +
'0<test-wrapper>123456789' +
'<!--comment2-->' +
'<span></span>0</test-wrapper>123456789'
@@ -277,39 +289,39 @@
const container = document.createElement('div');
container.textContent = fullText;
const attributes = {
- 'href': 'foo',
+ href: 'foo',
'data-foo': 'bar',
- 'class': 'hello world',
+ class: 'hello world',
};
GrAnnotation.annotateWithElement(container, 1, length, {
tagName: 'test-wrapper',
attributes,
});
assert(
- mockSanitize.calledWith(
- 'foo',
- 'href',
- 'attribute',
- sinon.match.instanceOf(Element)
- )
+ mockSanitize.calledWith(
+ 'foo',
+ 'href',
+ 'attribute',
+ sinon.match.instanceOf(Element)
+ )
);
assert(
- mockSanitize.calledWith(
- 'bar',
- 'data-foo',
- 'attribute',
- sinon.match.instanceOf(Element)
- )
+ mockSanitize.calledWith(
+ 'bar',
+ 'data-foo',
+ 'attribute',
+ sinon.match.instanceOf(Element)
+ )
);
assert(
- mockSanitize.calledWith(
- 'hello world',
- 'class',
- 'attribute',
- sinon.match.instanceOf(Element)
- )
+ mockSanitize.calledWith(
+ 'hello world',
+ 'class',
+ 'attribute',
+ sinon.match.instanceOf(Element)
+ )
);
- const el = container.querySelector('test-wrapper');
+ const el = container.querySelector('test-wrapper')!;
assert.equal(el.getAttribute('href'), 'foo');
assert.equal(el.getAttribute('data-foo'), 'bar');
assert.equal(el.getAttribute('class'), 'hello world');
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
index af921e4..f04e6a2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
@@ -5,7 +5,7 @@
*/
import '../../../test/common-test-setup';
import './gr-diff-highlight';
-import {_getTextOffset} from './gr-range-normalizer';
+import {getTextOffset} from './gr-range-normalizer';
import {fixture, fixtureCleanup, html, assert} from '@open-wc/testing';
import {
GrDiffHighlight,
@@ -62,7 +62,7 @@
<tr class="diff-row side-by-side" left-type="remove" right-type="add">
<td class="left lineNum" data-value="140"></td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
+ <td class="content remove"><div class="contentText"><!-- a comment node -->na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
[Yet another random diff thread content here]
</div></td>
<td class="right lineNum" data-value="120"></td>
@@ -684,13 +684,13 @@
if (!content.lastChild) assert.fail('last child of content not found');
let child = content.lastChild.lastChild;
if (!child) assert.fail('last child of last child of content not found');
- let result = _getTextOffset(content, child);
+ let result = getTextOffset(content, child);
assert.equal(result, 75);
content = stubContent(146, Side.RIGHT);
if (!content) assert.fail('content element not found');
child = content.lastChild;
if (!child) assert.fail('child element not found');
- result = _getTextOffset(content, child);
+ result = getTextOffset(content, child);
assert.equal(result, 0);
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-range-normalizer.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-range-normalizer.ts
index 9f23162..b177e14 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-range-normalizer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-range-normalizer.ts
@@ -25,12 +25,12 @@
* for syntax highlighting.
*/
export function normalize(range: Range): NormalizedRange {
- const startContainer = _getContentTextParent(range.startContainer);
+ const startContainer = getContentTextParent(range.startContainer);
const startOffset =
- range.startOffset + _getTextOffset(startContainer, range.startContainer);
- const endContainer = _getContentTextParent(range.endContainer);
+ range.startOffset + getTextOffset(startContainer, range.startContainer);
+ const endContainer = getContentTextParent(range.endContainer);
const endOffset =
- range.endOffset + _getTextOffset(endContainer, range.endContainer);
+ range.endOffset + getTextOffset(endContainer, range.endContainer);
return {
startContainer,
startOffset,
@@ -39,7 +39,7 @@
};
}
-function _getContentTextParent(target: Node): Node {
+function getContentTextParent(target: Node): Node {
if (!target.parentElement) return target;
let element: Element | null;
@@ -67,7 +67,7 @@
* @param child The child element being searched for.
*/
// TODO(TS): Only export for test.
-export function _getTextOffset(node: Node | null, child: Node): number {
+export function getTextOffset(node: Node | null, child: Node): number {
let count = 0;
let stack = [node];
while (stack.length) {
@@ -83,7 +83,7 @@
arr.reverse();
stack = stack.concat(arr);
} else {
- count += _getLength(n);
+ count += getLength(n);
}
}
return count;
@@ -96,8 +96,8 @@
* @param node A text node.
* @return The length of the text.
*/
-function _getLength(node?: Node | null) {
- return node && node.textContent
+function getLength(node?: Node | null) {
+ return node && node.textContent && node.nodeType !== Node.COMMENT_NODE
? node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length
: 0;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 5caffe6..5058ce8 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -11,12 +11,12 @@
import {customElement, property, state} from 'lit/decorators.js';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import {FixIronA11yAnnouncer} from '../../../types/types';
-import {getAppContext} from '../../../services/app-context';
import {fireIronAnnounce} from '../../../utils/event-util';
import {browserModelToken} from '../../../models/browser/browser-model';
import {resolve} from '../../../models/dependency';
import {css, html, LitElement} from 'lit';
import {sharedStyles} from '../../../styles/shared-styles';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-diff-mode-selector')
export class GrDiffModeSelector extends LitElement {
@@ -34,7 +34,7 @@
private readonly getBrowserModel = resolve(this, browserModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private subscriptions: Subscription[] = [];
@@ -118,7 +118,7 @@
*/
private setMode(newMode: DiffViewMode) {
if (this.saveOnChange && this.mode && this.mode !== newMode) {
- this.userModel.updatePreferences({diff_view: newMode});
+ this.getUserModel().updatePreferences({diff_view: newMode});
}
this.mode = newMode;
let announcement;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 34af01e..d646988 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -7,21 +7,17 @@
import './gr-diff-mode-selector';
import {GrDiffModeSelector} from './gr-diff-mode-selector';
import {DiffViewMode} from '../../../constants/constants';
-import {
- queryAndAssert,
- stubUsers,
- waitUntilObserved,
-} from '../../../test/test-utils';
+import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
import {fixture, html, assert} from '@open-wc/testing';
import {wrapInProvider} from '../../../models/di-provider-element';
import {
BrowserModel,
browserModelToken,
} from '../../../models/browser/browser-model';
-import {getAppContext} from '../../../services/app-context';
-import {UserModel} from '../../../models/user/user-model';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
import {createPreferences} from '../../../test/test-data-generators';
import {GrButton} from '../../../elements/shared/gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-diff-mode-selector tests', () => {
let element: GrDiffModeSelector;
@@ -29,7 +25,7 @@
let userModel: UserModel;
setup(async () => {
- userModel = getAppContext().userModel;
+ userModel = testResolver(userModelToken);
browserModel = new BrowserModel(userModel);
element = (
await fixture(
@@ -129,7 +125,7 @@
test('set mode', async () => {
browserModel.setScreenWidth(0);
- const saveStub = stubUsers('updatePreferences');
+ const saveStub = sinon.stub(userModel, 'updatePreferences');
// Setting the mode initially does not save prefs.
element.saveOnChange = true;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index 062347f..9f874b8 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -15,7 +15,7 @@
GrDiffGroupType,
hideInContextControl,
} from '../gr-diff/gr-diff-group';
-import {CancelablePromise, makeCancelable} from '../../../scripts/util';
+import {CancelablePromise, makeCancelable} from '../../../utils/async-util';
import {DiffContent} from '../../../types/diff';
import {Side} from '../../../constants/constants';
import {debounce, DelayedTask} from '../../../utils/async-util';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index 4bb8cc3..db53eae 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -8,7 +8,11 @@
normalize,
NormalizedRange,
} from '../gr-diff-highlight/gr-range-normalizer';
-import {descendedFromClass, querySelectorAll} from '../../../utils/dom-util';
+import {
+ descendedFromClass,
+ parentWithClass,
+ querySelectorAll,
+} from '../../../utils/dom-util';
import {DiffInfo} from '../../../types/diff';
import {Side} from '../../../constants/constants';
import {
@@ -30,6 +34,10 @@
BLAME: 'selected-blame',
};
+function selectionClassForSide(side?: Side) {
+ return side === Side.LEFT ? SelectionClass.LEFT : SelectionClass.RIGHT;
+}
+
interface LinesCache {
left: string[] | null;
right: string[] | null;
@@ -65,52 +73,31 @@
this.diffTable.removeEventListener('mousedown', this.handleDown);
}
- handleDownOnRangeComment(node: Element) {
- if (isThreadEl(node)) {
- this.setClasses([
- SelectionClass.COMMENT,
- getSide(node) === Side.LEFT
- ? SelectionClass.LEFT
- : SelectionClass.RIGHT,
- ]);
- return true;
- }
- return false;
- }
-
handleDown = (e: Event) => {
const target = e.target;
if (!(target instanceof Element)) return;
- const handled = this.handleDownOnRangeComment(target);
- if (handled) return;
- const lineEl = getLineElByChild(target);
- const blameSelected = descendedFromClass(target, 'blame', this.diffTable);
- if (!lineEl && !blameSelected) {
+
+ const commentEl = parentWithClass(target, 'comment-thread', this.diffTable);
+ if (commentEl && isThreadEl(commentEl)) {
+ this.setClasses([
+ SelectionClass.COMMENT,
+ selectionClassForSide(getSide(commentEl)),
+ ]);
return;
}
- const targetClasses = [];
-
+ const blameSelected = descendedFromClass(target, 'blame', this.diffTable);
if (blameSelected) {
- targetClasses.push(SelectionClass.BLAME);
- } else if (lineEl) {
- const commentSelected = descendedFromClass(
- target,
- 'gr-comment',
- this.diffTable
- );
- const side = getSideByLineEl(lineEl);
-
- targetClasses.push(
- side === 'left' ? SelectionClass.LEFT : SelectionClass.RIGHT
- );
-
- if (commentSelected) {
- targetClasses.push(SelectionClass.COMMENT);
- }
+ this.setClasses([SelectionClass.BLAME]);
+ return;
}
- this.setClasses(targetClasses);
+ // This works for both, the content and the line number cells.
+ const lineEl = getLineElByChild(target);
+ if (lineEl) {
+ this.setClasses([selectionClassForSide(getSideByLineEl(lineEl))]);
+ return;
+ }
};
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index 5a34ae3..bb04245 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -65,7 +65,7 @@
// because then that row would consume as much space as the collapsed code.
if (numHidden > 3) {
if (hiddenStart) {
- [before, hidden] = _splitCommonGroups(hidden, hiddenStart);
+ [before, hidden] = splitCommonGroups(hidden, hiddenStart);
}
if (hiddenEnd) {
let beforeLength = 0;
@@ -74,7 +74,7 @@
const beforeEnd = before[before.length - 1].lineRange.left.end_line;
beforeLength = beforeEnd - beforeStart + 1;
}
- [hidden, after] = _splitCommonGroups(hidden, hiddenEnd - beforeLength);
+ [hidden, after] = splitCommonGroups(hidden, hiddenEnd - beforeLength);
}
} else {
[hidden, after] = [[], hidden];
@@ -95,7 +95,7 @@
/**
* Splits a group in two, defined by leftSplit and rightSplit. Primarily to be
- * used in function _splitCommonGroups
+ * used in function splitCommonGroups
* Groups with some lines before and some lines after the split will be split
* into two groups, which will be put into the first and second list.
*
@@ -104,7 +104,7 @@
* @param rightSplit The line number relative to the split on the right side
* @return two new groups, one before the split and another after it
*/
-function _splitGroupInTwo(
+function splitGroupInTwo(
group: GrDiffGroup,
leftSplit: number,
rightSplit: number
@@ -167,7 +167,7 @@
* @return The outer array has 2 elements, the
* list of groups before and the list of groups after the split.
*/
-function _splitCommonGroups(
+function splitCommonGroups(
groups: readonly GrDiffGroup[],
split: number
): GrDiffGroup[][] {
@@ -189,7 +189,7 @@
} else if (isCompletelyAfter) {
afterGroups.push(group);
} else {
- const {beforeSplit, afterSplit} = _splitGroupInTwo(
+ const {beforeSplit, afterSplit} = splitGroupInTwo(
group,
leftSplit,
rightSplit
@@ -471,7 +471,15 @@
return Promise.resolve();
}
assertIsDefined(this.element);
- await untilRendered(this.element);
+ // This is a temporary hack while migration to lit based diff rendering:
+ // Elements with 'display: contents;' do not have a height, so they
+ // won't work as intended with `untilRendered()`.
+ const watchEl =
+ this.element.tagName === 'GR-DIFF-SECTION'
+ ? this.element.firstElementChild
+ : this.element;
+ assertIsDefined(watchEl);
+ await untilRendered(watchEl as HTMLElement);
}
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
index 7ca4a03..338a275 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
@@ -7,6 +7,7 @@
GrDiffLine as GrDiffLineApi,
GrDiffLineType,
LineNumber,
+ Side,
} from '../../../api/diff';
export {GrDiffLineType};
@@ -27,6 +28,10 @@
text = '';
+ lineNumber(side: Side) {
+ return side === Side.LEFT ? this.beforeNumber : this.afterNumber;
+ }
+
// TODO(TS): remove this properties
static readonly Type = GrDiffLineType;
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 2b61c8c..8a17611 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
@@ -34,12 +34,20 @@
* Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
* A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
*/
-const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+export const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
// If any line of the diff is more than the character limit, then disable
// syntax highlighting for the entire file.
export const SYNTAX_MAX_LINE_LENGTH = 500;
+export function countLines(diff?: DiffInfo, side?: Side) {
+ if (!diff?.content || !side) return 0;
+ return diff.content.reduce((sum, chunk) => {
+ const sideChunk = side === Side.LEFT ? chunk.a : chunk.b;
+ return sum + (sideChunk?.length ?? chunk.ab?.length ?? chunk.skip ?? 0);
+ }, 0);
+}
+
export function getResponsiveMode(
prefs: DiffPreferencesInfo,
renderPrefs?: RenderPreferences
@@ -54,7 +62,7 @@
return 'NONE';
}
-export function isResponsive(responsiveMode: DiffResponsiveMode) {
+export function isResponsive(responsiveMode?: DiffResponsiveMode) {
return (
responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY'
);
@@ -105,7 +113,12 @@
return null;
}
}
- node = node.previousSibling ?? node.parentElement ?? undefined;
+ node =
+ (node as Element).assignedSlot ??
+ (node as ShadowRoot).host ??
+ node.previousSibling ??
+ node.parentNode ??
+ undefined;
}
return null;
}
@@ -184,9 +197,16 @@
}
/**
+ * Simple helper method for creating element classes in the context of
+ * gr-diff. This is just a super simple convenience function.
+ */
+export function diffClasses(...additionalClasses: string[]) {
+ return ['gr-diff', ...additionalClasses].join(' ');
+}
+
+/**
* Simple helper method for creating elements in the context of gr-diff.
- *
- * Otherwise this is just a super simple convenience function.
+ * This is just a super simple convenience function.
*/
export function createElementDiff(
tagName: string,
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 53c2780..28097df 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -27,7 +27,6 @@
isResponsive,
getDiffLength,
} from './gr-diff-utils';
-import {getHiddenScroll} from '../../../scripts/hiddenscroll';
import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
@@ -69,6 +68,7 @@
import {grRangedCommentTheme} from '../gr-ranged-comment-themes/gr-ranged-comment-theme';
import {classMap} from 'lit/directives/class-map.js';
import {iconStyles} from '../../../styles/gr-icon-styles';
+import {expandFileMode} from '../../../utils/file-util';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -314,9 +314,6 @@
display: flex;
font-family: var(--monospace-font-family);
}
- .diffContainer.hiddenscroll {
- margin-bottom: var(--spacing-m);
- }
table {
border-collapse: collapse;
table-layout: fixed;
@@ -765,7 +762,7 @@
}
#loadingError,
#sizeWarning {
- display: none;
+ display: block;
margin: var(--spacing-l) auto;
max-width: 60em;
text-align: center;
@@ -776,23 +773,19 @@
#sizeWarning gr-button {
margin: var(--spacing-l);
}
- #loadingError.showError,
- #sizeWarning.warn {
- display: block;
- }
.target-row td.blame {
background: var(--diff-selection-background-color);
}
td.lost div {
background-color: var(--info-background);
- padding: var(--spacing-s) 0 0 0;
}
- td.lost div:first-of-type {
+ td.lost div.lost-message {
font-family: var(--font-family, 'Roboto');
font-size: var(--font-size-normal, 14px);
line-height: var(--line-height-normal);
+ padding: var(--spacing-s) 0;
}
- td.lost gr-icon {
+ td.lost div.lost-message gr-icon {
padding: 0 var(--spacing-s) 0 var(--spacing-m);
color: var(--blue-700);
}
@@ -966,6 +959,12 @@
*/
z-index: 10;
}
+
+ gr-diff-section,
+ gr-context-controls-section,
+ gr-diff-row {
+ display: contents;
+ }
`,
];
}
@@ -1061,7 +1060,6 @@
diffContainer: true,
unified: this.viewMode === DiffViewMode.UNIFIED,
sideBySide: this.viewMode === DiffViewMode.SIDE_BY_SIDE,
- hiddenscroll: !!getHiddenScroll(),
canComment: this.loggedIn,
displayLine: this.displayLine,
};
@@ -1087,24 +1085,20 @@
private renderNewlineWarning() {
const newlineWarning = this.computeNewlineWarning();
- const newlineWarningClass = this.computeNewlineWarningClass(
- !!newlineWarning
- );
- return html` <div class=${newlineWarningClass}>${newlineWarning}</div> `;
+ if (!newlineWarning) return nothing;
+ return html`<div class="newlineWarning">${newlineWarning}</div>`;
}
private renderLoadingError() {
- return html`
- <div id="loadingError" class=${this.errorMessage ? 'showError' : ''}>
- ${this.errorMessage}
- </div>
- `;
+ if (!this.errorMessage) return nothing;
+ return html`<div id="loadingError">${this.errorMessage}</div>`;
}
private renderSizeWarning() {
+ if (!this.showWarning) return nothing;
// TODO: Update comment about 'Whole file' as it's not in settings.
return html`
- <div id="sizeWarning" class=${this.showWarning ? 'warn' : ''}>
+ <div id="sizeWarning">
<p>
Prevented render because "Whole file" is enabled and this diff is very
large (about ${this.diffLength} lines).
@@ -1685,8 +1679,8 @@
}
const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) continue;
- if (lineNum === 'LOST' && !contentEl.hasChildNodes()) {
- contentEl.appendChild(this.portedCommentsWithoutRangeMessage());
+ if (lineNum === 'LOST') {
+ this.insertPortedCommentsWithoutRangeMessage(contentEl);
}
const threadGroupEl = this.getOrCreateThreadGroup(contentEl, commentSide);
@@ -1733,15 +1727,19 @@
this.commentRanges = [];
}
- private portedCommentsWithoutRangeMessage() {
+ private insertPortedCommentsWithoutRangeMessage(lostCell: Element) {
+ const existingMessage = lostCell.querySelector('div.lost-message');
+ if (existingMessage) return;
+
const div = document.createElement('div');
+ div.className = 'lost-message';
const icon = document.createElement('gr-icon');
icon.setAttribute('icon', 'info');
div.appendChild(icon);
const span = document.createElement('span');
span.innerText = 'Original comment position not found in this patchset';
div.appendChild(span);
- return div;
+ lostCell.insertBefore(div, lostCell.firstChild);
}
/**
@@ -1765,19 +1763,18 @@
// Private but used in tests.
computeDiffHeaderItems() {
- if (!this.diff || !this.diff.diff_header) {
- return [];
- }
- return this.diff.diff_header.filter(
- item =>
- !(
- item.startsWith('diff --git ') ||
- item.startsWith('index ') ||
- item.startsWith('+++ ') ||
- item.startsWith('--- ') ||
- item === 'Binary files differ'
- )
- );
+ return (this.diff?.diff_header ?? [])
+ .filter(
+ item =>
+ !(
+ item.startsWith('diff --git ') ||
+ item.startsWith('index ') ||
+ item.startsWith('+++ ') ||
+ item.startsWith('--- ') ||
+ item === 'Binary files differ'
+ )
+ )
+ .map(expandFileMode);
}
private handleFullBypass() {
@@ -1805,7 +1802,7 @@
}
}
- private computeNewlineWarning() {
+ private computeNewlineWarning(): string | undefined {
const messages = [];
if (this.showNewlineWarningLeft) {
messages.push(NO_NEWLINE_LEFT);
@@ -1814,18 +1811,10 @@
messages.push(NO_NEWLINE_RIGHT);
}
if (!messages.length) {
- return null;
+ return undefined;
}
return messages.join(' \u2014 '); // \u2014 - '—'
}
-
- // Private but used in tests.
- computeNewlineWarningClass(warning: boolean) {
- if (this.loading || !warning) {
- return 'newlineWarning hidden';
- }
- return 'newlineWarning';
- }
}
function extractAddedNodes(mutations: MutationRecord[]) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index 59bfc8d..f657973 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -8,7 +8,6 @@
import './gr-diff';
import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image';
import {getComputedStyleValue} from '../../../utils/dom-util';
-import {_setHiddenScroll} from '../../../scripts/hiddenscroll';
import '@polymer/paper-button/paper-button';
import {
DiffContent,
@@ -57,6 +56,1777 @@
element = await fixture<GrDiff>(html`<gr-diff></gr-diff>`);
});
+ suite('rendering', () => {
+ test('empty diff', async () => {
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="diffContainer sideBySide">
+ <table id="diffTable"></table>
+ </div>
+ `
+ );
+ });
+
+ test('a normal diff legacy', async () => {
+ await testNormal();
+ });
+
+ test('a normal diff lit', async () => {
+ // TODO(brohlfs): Make sure that test passes. Then uncomment next line.
+ // element.renderPrefs = {...element.renderPrefs, use_lit_components: true};
+ await testNormal();
+ });
+
+ const testNormal = async () => {
+ element.prefs = {...MINIMAL_PREFS};
+ element.diff = createDiff();
+ await element.updateComplete;
+ await waitForEventOnce(element, 'render');
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="diffContainer sideBySide">
+ <table class="selected-right" id="diffTable">
+ <colgroup>
+ <col class="blame gr-diff" />
+ <col class="gr-diff left" width="48" />
+ <col class="gr-diff left sign" />
+ <col class="gr-diff left" />
+ <col width="48" />
+ <col class="gr-diff right sign" />
+ <col />
+ </colgroup>
+ <tbody class="both gr-diff section">
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="LOST"></td>
+ <td class="gr-diff left lineNum" data-value="LOST"></td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td
+ class="both content gr-diff left lost no-intraline-info"
+ ></td>
+ <td class="gr-diff lineNum right" data-value="LOST"></td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td
+ class="both content gr-diff lost no-intraline-info right"
+ ></td>
+ </tr>
+ </tbody>
+ <tbody class="both gr-diff section">
+ <tr
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="FILE"></td>
+ <td class="gr-diff left lineNum" data-value="FILE">
+ <button
+ aria-label="Add file comment"
+ class="gr-diff left lineNumButton"
+ data-value="FILE"
+ id="left-button-FILE"
+ tabindex="-1"
+ >
+ File
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td
+ class="both content file gr-diff left no-intraline-info"
+ ></td>
+ <td class="gr-diff lineNum right" data-value="FILE">
+ <button
+ aria-label="Add file comment"
+ class="gr-diff lineNumButton right"
+ data-value="FILE"
+ id="right-button-FILE"
+ tabindex="-1"
+ >
+ File
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td
+ class="both content file gr-diff no-intraline-info right"
+ ></td>
+ </tr>
+ </tbody>
+ <tbody class="both gr-diff section">
+ <tr
+ aria-labelledby="left-button-1 left-content-1 right-button-1 right-content-1"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="1"></td>
+ <td class="gr-diff left lineNum" data-value="1">
+ <button
+ aria-label="1 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="1"
+ id="left-button-1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-1"
+ >
+ Lorem ipsum dolor sit amet, suspendisse inceptos vehicula.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="1">
+ <button
+ aria-label="1 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="1"
+ id="right-button-1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-1"
+ >
+ Lorem ipsum dolor sit amet, suspendisse inceptos vehicula.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-2 left-content-2 right-button-2 right-content-2"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="2"></td>
+ <td class="gr-diff left lineNum" data-value="2">
+ <button
+ aria-label="2 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="2"
+ id="left-button-2"
+ tabindex="-1"
+ >
+ 2
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-2"
+ >
+ Mattis lectus.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="2">
+ <button
+ aria-label="2 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="2"
+ id="right-button-2"
+ tabindex="-1"
+ >
+ 2
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-2"
+ >
+ Mattis lectus.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-3 left-content-3 right-button-3 right-content-3"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="3"></td>
+ <td class="gr-diff left lineNum" data-value="3">
+ <button
+ aria-label="3 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="3"
+ id="left-button-3"
+ tabindex="-1"
+ >
+ 3
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-3"
+ >
+ Sodales duis.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="3">
+ <button
+ aria-label="3 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="3"
+ id="right-button-3"
+ tabindex="-1"
+ >
+ 3
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-3"
+ >
+ Sodales duis.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-4 left-content-4 right-button-4 right-content-4"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="4"></td>
+ <td class="gr-diff left lineNum" data-value="4">
+ <button
+ aria-label="4 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="4"
+ id="left-button-4"
+ tabindex="-1"
+ >
+ 4
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-4"
+ >
+ Orci a faucibus.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="4">
+ <button
+ aria-label="4 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="4"
+ id="right-button-4"
+ tabindex="-1"
+ >
+ 4
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-4"
+ >
+ Orci a faucibus.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="delta gr-diff section total">
+ <tr
+ aria-labelledby="right-button-5 right-content-5"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="5">
+ <button
+ aria-label="5 added"
+ class="gr-diff lineNumButton right"
+ data-value="5"
+ id="right-button-5"
+ tabindex="-1"
+ >
+ 5
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-5"
+ >
+ Nullam neque, ligula ac, id blandit.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="right-button-6 right-content-6"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="6">
+ <button
+ aria-label="6 added"
+ class="gr-diff lineNumButton right"
+ data-value="6"
+ id="right-button-6"
+ tabindex="-1"
+ >
+ 6
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-6"
+ >
+ Sagittis tincidunt torquent, tempor nunc amet.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="right-button-7 right-content-7"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="7">
+ <button
+ aria-label="7 added"
+ class="gr-diff lineNumButton right"
+ data-value="7"
+ id="right-button-7"
+ tabindex="-1"
+ >
+ 7
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-7"
+ >
+ At rhoncus id.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="both gr-diff section">
+ <tr
+ aria-labelledby="left-button-5 left-content-5 right-button-8 right-content-8"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="5"></td>
+ <td class="gr-diff left lineNum" data-value="5">
+ <button
+ aria-label="5 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="5"
+ id="left-button-5"
+ tabindex="-1"
+ >
+ 5
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-5"
+ >
+ Sem nascetur, erat ut, non in.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="8">
+ <button
+ aria-label="8 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="8"
+ id="right-button-8"
+ tabindex="-1"
+ >
+ 8
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-8"
+ >
+ Sem nascetur, erat ut, non in.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-6 left-content-6 right-button-9 right-content-9"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="6"></td>
+ <td class="gr-diff left lineNum" data-value="6">
+ <button
+ aria-label="6 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="6"
+ id="left-button-6"
+ tabindex="-1"
+ >
+ 6
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-6"
+ >
+ A donec, venenatis pellentesque dis.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="9">
+ <button
+ aria-label="9 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="9"
+ id="right-button-9"
+ tabindex="-1"
+ >
+ 9
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-9"
+ >
+ A donec, venenatis pellentesque dis.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-7 left-content-7 right-button-10 right-content-10"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="7"></td>
+ <td class="gr-diff left lineNum" data-value="7">
+ <button
+ aria-label="7 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="7"
+ id="left-button-7"
+ tabindex="-1"
+ >
+ 7
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-7"
+ >
+ Mauris mauris.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="10">
+ <button
+ aria-label="10 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="10"
+ id="right-button-10"
+ tabindex="-1"
+ >
+ 10
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-10"
+ >
+ Mauris mauris.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-8 left-content-8 right-button-11 right-content-11"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="8"></td>
+ <td class="gr-diff left lineNum" data-value="8">
+ <button
+ aria-label="8 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="8"
+ id="left-button-8"
+ tabindex="-1"
+ >
+ 8
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-8"
+ >
+ Quisque nisl duis, facilisis viverra.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="11">
+ <button
+ aria-label="11 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="11"
+ id="right-button-11"
+ tabindex="-1"
+ >
+ 11
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-11"
+ >
+ Quisque nisl duis, facilisis viverra.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-9 left-content-9 right-button-12 right-content-12"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="9"></td>
+ <td class="gr-diff left lineNum" data-value="9">
+ <button
+ aria-label="9 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="9"
+ id="left-button-9"
+ tabindex="-1"
+ >
+ 9
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-9"
+ >
+ Justo purus, semper eget et.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="12">
+ <button
+ aria-label="12 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="12"
+ id="right-button-12"
+ tabindex="-1"
+ >
+ 12
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-12"
+ >
+ Justo purus, semper eget et.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="delta gr-diff section total">
+ <tr
+ aria-labelledby="left-button-10 left-content-10"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="blank"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="10"></td>
+ <td class="gr-diff left lineNum" data-value="10">
+ <button
+ aria-label="10 removed"
+ class="gr-diff left lineNumButton"
+ data-value="10"
+ id="left-button-10"
+ tabindex="-1"
+ >
+ 10
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info remove sign">-</td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-10"
+ >
+ Est amet, vestibulum pellentesque.
+ </div>
+ </td>
+ <td class="blankLineNum gr-diff right"></td>
+ <td class="blank gr-diff no-intraline-info right sign"></td>
+ <td class="blank gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-0"
+ ></div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-11 left-content-11"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="blank"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="11"></td>
+ <td class="gr-diff left lineNum" data-value="11">
+ <button
+ aria-label="11 removed"
+ class="gr-diff left lineNumButton"
+ data-value="11"
+ id="left-button-11"
+ tabindex="-1"
+ >
+ 11
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info remove sign">-</td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-11"
+ >
+ Erat ligula.
+ </div>
+ </td>
+ <td class="blankLineNum gr-diff right"></td>
+ <td class="blank gr-diff no-intraline-info right sign"></td>
+ <td class="blank gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-0"
+ ></div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-12 left-content-12"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="blank"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="12"></td>
+ <td class="gr-diff left lineNum" data-value="12">
+ <button
+ aria-label="12 removed"
+ class="gr-diff left lineNumButton"
+ data-value="12"
+ id="left-button-12"
+ tabindex="-1"
+ >
+ 12
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info remove sign">-</td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-12"
+ >
+ Justo eros.
+ </div>
+ </td>
+ <td class="blankLineNum gr-diff right"></td>
+ <td class="blank gr-diff no-intraline-info right sign"></td>
+ <td class="blank gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-0"
+ ></div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-13 left-content-13"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="blank"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="13"></td>
+ <td class="gr-diff left lineNum" data-value="13">
+ <button
+ aria-label="13 removed"
+ class="gr-diff left lineNumButton"
+ data-value="13"
+ id="left-button-13"
+ tabindex="-1"
+ >
+ 13
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info remove sign">-</td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-13"
+ >
+ Fringilla quisque.
+ </div>
+ </td>
+ <td class="blankLineNum gr-diff right"></td>
+ <td class="blank gr-diff no-intraline-info right sign"></td>
+ <td class="blank gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-0"
+ ></div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="delta gr-diff ignoredWhitespaceOnly section">
+ <tr
+ aria-labelledby="left-button-14 left-content-14 right-button-13 right-content-13"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="14"></td>
+ <td class="gr-diff left lineNum" data-value="14">
+ <button
+ aria-label="14 removed"
+ class="gr-diff left lineNumButton"
+ data-value="14"
+ id="left-button-14"
+ tabindex="-1"
+ >
+ 14
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info remove sign">-</td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-14"
+ >
+ Arcu eget, rhoncus amet cursus, ipsum elementum.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="13">
+ <button
+ aria-label="13 added"
+ class="gr-diff lineNumButton right"
+ data-value="13"
+ id="right-button-13"
+ tabindex="-1"
+ >
+ 13
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-13"
+ >
+ Arcu eget, rhoncus amet cursus, ipsum elementum.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-15 left-content-15 right-button-14 right-content-14"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="15"></td>
+ <td class="gr-diff left lineNum" data-value="15">
+ <button
+ aria-label="15 removed"
+ class="gr-diff left lineNumButton"
+ data-value="15"
+ id="left-button-15"
+ tabindex="-1"
+ >
+ 15
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info remove sign">-</td>
+ <td class="content gr-diff left no-intraline-info remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-15"
+ >
+ Eros suspendisse.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="14">
+ <button
+ aria-label="14 added"
+ class="gr-diff lineNumButton right"
+ data-value="14"
+ id="right-button-14"
+ tabindex="-1"
+ >
+ 14
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-14"
+ >
+ Eros suspendisse.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="delta gr-diff section">
+ <tr
+ aria-labelledby="left-button-16 left-content-16 right-button-15 right-content-15"
+ class="diff-row gr-diff side-by-side"
+ left-type="remove"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="16"></td>
+ <td class="gr-diff left lineNum" data-value="16">
+ <button
+ aria-label="16 removed"
+ class="gr-diff left lineNumButton"
+ data-value="16"
+ id="left-button-16"
+ tabindex="-1"
+ >
+ 16
+ </button>
+ </td>
+ <td class="gr-diff left remove sign">-</td>
+ <td class="content gr-diff left remove">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-16"
+ >
+ Rhoncus tempor, ultricies
+ <hl class="gr-diff intraline"> aliquam </hl> ipsum.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="15">
+ <button
+ aria-label="15 added"
+ class="gr-diff lineNumButton right"
+ data-value="15"
+ id="right-button-15"
+ tabindex="-1"
+ >
+ 15
+ </button>
+ </td>
+ <td class="add gr-diff right sign">+</td>
+ <td class="add content gr-diff right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-15"
+ >
+ Rhoncus tempor, ultricies
+ <hl class="gr-diff intraline"> praesent </hl> ipsum.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="both gr-diff section">
+ <tr
+ aria-labelledby="left-button-17 left-content-17 right-button-16 right-content-16"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="17"></td>
+ <td class="gr-diff left lineNum" data-value="17">
+ <button
+ aria-label="17 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="17"
+ id="left-button-17"
+ tabindex="-1"
+ >
+ 17
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-17"
+ >
+ Sollicitudin duis.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="16">
+ <button
+ aria-label="16 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="16"
+ id="right-button-16"
+ tabindex="-1"
+ >
+ 16
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-16"
+ >
+ Sollicitudin duis.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-18 left-content-18 right-button-17 right-content-17"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="18"></td>
+ <td class="gr-diff left lineNum" data-value="18">
+ <button
+ aria-label="18 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="18"
+ id="left-button-18"
+ tabindex="-1"
+ >
+ 18
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-18"
+ >
+ Blandit blandit, ante nisl fusce.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="17">
+ <button
+ aria-label="17 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="17"
+ id="right-button-17"
+ tabindex="-1"
+ >
+ 17
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-17"
+ >
+ Blandit blandit, ante nisl fusce.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-19 left-content-19 right-button-18 right-content-18"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="19"></td>
+ <td class="gr-diff left lineNum" data-value="19">
+ <button
+ aria-label="19 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="19"
+ id="left-button-19"
+ tabindex="-1"
+ >
+ 19
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-19"
+ >
+ Felis ac at, tellus consectetuer.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="18">
+ <button
+ aria-label="18 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="18"
+ id="right-button-18"
+ tabindex="-1"
+ >
+ 18
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-18"
+ >
+ Felis ac at, tellus consectetuer.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="contextControl gr-diff section">
+ <tr
+ class="above contextBackground gr-diff side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff sign"></td>
+ <td class="gr-diff"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff sign"></td>
+ <td class="gr-diff"></td>
+ </tr>
+ <tr class="dividerRow gr-diff show-both">
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="gr-diff"></td>
+ <td class="dividerCell gr-diff" colspan="3">
+ <gr-context-controls
+ class="gr-diff"
+ showconfig="both"
+ ></gr-context-controls>
+ </td>
+ </tr>
+ <tr
+ class="below contextBackground gr-diff side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff sign"></td>
+ <td class="gr-diff"></td>
+ <td class="contextLineNum gr-diff"></td>
+ <td class="gr-diff sign"></td>
+ <td class="gr-diff"></td>
+ </tr>
+ </tbody>
+ <tbody class="both gr-diff section">
+ <tr
+ aria-labelledby="left-button-38 left-content-38 right-button-37 right-content-37"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="38"></td>
+ <td class="gr-diff left lineNum" data-value="38">
+ <button
+ aria-label="38 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="38"
+ id="left-button-38"
+ tabindex="-1"
+ >
+ 38
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-38"
+ >
+ Ullamcorper nunc ante, nec imperdiet felis, consectetur.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="37">
+ <button
+ aria-label="37 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="37"
+ id="right-button-37"
+ tabindex="-1"
+ >
+ 37
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-37"
+ >
+ Ullamcorper nunc ante, nec imperdiet felis, consectetur.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-39 left-content-39 right-button-38 right-content-38"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="39"></td>
+ <td class="gr-diff left lineNum" data-value="39">
+ <button
+ aria-label="39 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="39"
+ id="left-button-39"
+ tabindex="-1"
+ >
+ 39
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-39"
+ >
+ Ac eget.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="38">
+ <button
+ aria-label="38 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="38"
+ id="right-button-38"
+ tabindex="-1"
+ >
+ 38
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-38"
+ >
+ Ac eget.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-40 left-content-40 right-button-39 right-content-39"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="40"></td>
+ <td class="gr-diff left lineNum" data-value="40">
+ <button
+ aria-label="40 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="40"
+ id="left-button-40"
+ tabindex="-1"
+ >
+ 40
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-40"
+ >
+ Vel fringilla, interdum pellentesque placerat, proin ante.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="39">
+ <button
+ aria-label="39 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="39"
+ id="right-button-39"
+ tabindex="-1"
+ >
+ 39
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-39"
+ >
+ Vel fringilla, interdum pellentesque placerat, proin ante.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="delta gr-diff section total">
+ <tr
+ aria-labelledby="right-button-40 right-content-40"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="40">
+ <button
+ aria-label="40 added"
+ class="gr-diff lineNumButton right"
+ data-value="40"
+ id="right-button-40"
+ tabindex="-1"
+ >
+ 40
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-40"
+ >
+ Eu congue risus.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="right-button-41 right-content-41"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="41">
+ <button
+ aria-label="41 added"
+ class="gr-diff lineNumButton right"
+ data-value="41"
+ id="right-button-41"
+ tabindex="-1"
+ >
+ 41
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-41"
+ >
+ Enim ac, quis elementum.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="right-button-42 right-content-42"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="42">
+ <button
+ aria-label="42 added"
+ class="gr-diff lineNumButton right"
+ data-value="42"
+ id="right-button-42"
+ tabindex="-1"
+ >
+ 42
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-42"
+ >
+ Non et elit.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="right-button-43 right-content-43"
+ class="diff-row gr-diff side-by-side"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="0"></td>
+ <td class="blankLineNum gr-diff left"></td>
+ <td class="blank gr-diff left no-intraline-info sign"></td>
+ <td class="blank gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-0"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="43">
+ <button
+ aria-label="43 added"
+ class="gr-diff lineNumButton right"
+ data-value="43"
+ id="right-button-43"
+ tabindex="-1"
+ >
+ 43
+ </button>
+ </td>
+ <td class="add gr-diff no-intraline-info right sign">+</td>
+ <td class="add content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-43"
+ >
+ Etiam aliquam, diam vel nunc.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody class="both gr-diff section">
+ <tr
+ aria-labelledby="left-button-41 left-content-41 right-button-44 right-content-44"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="41"></td>
+ <td class="gr-diff left lineNum" data-value="41">
+ <button
+ aria-label="41 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="41"
+ id="left-button-41"
+ tabindex="-1"
+ >
+ 41
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-41"
+ >
+ Nec at.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="44">
+ <button
+ aria-label="44 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="44"
+ id="right-button-44"
+ tabindex="-1"
+ >
+ 44
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-44"
+ >
+ Nec at.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-42 left-content-42 right-button-45 right-content-45"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="42"></td>
+ <td class="gr-diff left lineNum" data-value="42">
+ <button
+ aria-label="42 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="42"
+ id="left-button-42"
+ tabindex="-1"
+ >
+ 42
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-42"
+ >
+ Arcu mauris, venenatis lacus fermentum, praesent duis.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="45">
+ <button
+ aria-label="45 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="45"
+ id="right-button-45"
+ tabindex="-1"
+ >
+ 45
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-45"
+ >
+ Arcu mauris, venenatis lacus fermentum, praesent duis.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-43 left-content-43 right-button-46 right-content-46"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="43"></td>
+ <td class="gr-diff left lineNum" data-value="43">
+ <button
+ aria-label="43 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="43"
+ id="left-button-43"
+ tabindex="-1"
+ >
+ 43
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-43"
+ >
+ Pellentesque amet et, tellus duis.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="46">
+ <button
+ aria-label="46 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="46"
+ id="right-button-46"
+ tabindex="-1"
+ >
+ 46
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-46"
+ >
+ Pellentesque amet et, tellus duis.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-44 left-content-44 right-button-47 right-content-47"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="44"></td>
+ <td class="gr-diff left lineNum" data-value="44">
+ <button
+ aria-label="44 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="44"
+ id="left-button-44"
+ tabindex="-1"
+ >
+ 44
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-44"
+ >
+ Ipsum arcu vitae, justo elit, sed libero tellus.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="47">
+ <button
+ aria-label="47 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="47"
+ id="right-button-47"
+ tabindex="-1"
+ >
+ 47
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-47"
+ >
+ Ipsum arcu vitae, justo elit, sed libero tellus.
+ </div>
+ </td>
+ </tr>
+ <tr
+ aria-labelledby="left-button-45 left-content-45 right-button-48 right-content-48"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff" data-line-number="45"></td>
+ <td class="gr-diff left lineNum" data-value="45">
+ <button
+ aria-label="45 unmodified"
+ class="gr-diff left lineNumButton"
+ data-value="45"
+ id="left-button-45"
+ tabindex="-1"
+ >
+ 45
+ </button>
+ </td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td class="both content gr-diff left no-intraline-info">
+ <div
+ class="contentText gr-diff"
+ data-side="left"
+ id="left-content-45"
+ >
+ Metus rutrum euismod, vivamus sodales, vel arcu nisl.
+ </div>
+ </td>
+ <td class="gr-diff lineNum right" data-value="48">
+ <button
+ aria-label="48 unmodified"
+ class="gr-diff lineNumButton right"
+ data-value="48"
+ id="right-button-48"
+ tabindex="-1"
+ >
+ 48
+ </button>
+ </td>
+ <td class="gr-diff no-intraline-info right sign"></td>
+ <td class="both content gr-diff no-intraline-info right">
+ <div
+ class="contentText gr-diff"
+ data-side="right"
+ id="right-content-48"
+ >
+ Metus rutrum euismod, vivamus sodales, vel arcu nisl.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ `
+ );
+ };
+ });
+
suite('selectionchange event handling', () => {
let handleSelectionChangeStub: sinon.SinonSpy;
@@ -567,14 +2337,6 @@
assert.isTrue(actual[actual.length - 1] instanceof AbortStop);
});
});
-
- test('adds .hiddenscroll', async () => {
- _setHiddenScroll(true);
- element.displayLine = true;
- await element.updateComplete;
- const container = queryAndAssert(element, '.diffContainer');
- assert.include(container.className, 'hiddenscroll');
- });
});
suite('logged in', async () => {
@@ -930,8 +2692,8 @@
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
const getWarning = (element: GrDiff) => {
- const warningElement = queryAndAssert(element, '.newlineWarning');
- return warningElement.textContent;
+ const warningElement = query(element, '.newlineWarning');
+ return warningElement?.textContent ?? '';
};
setup(async () => {
@@ -977,17 +2739,6 @@
assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
});
});
-
- test('computeNewlineWarningClass', () => {
- const hidden = 'newlineWarning hidden';
- const shown = 'newlineWarning';
- element.loading = true;
- assert.equal(element.computeNewlineWarningClass(false), hidden);
- assert.equal(element.computeNewlineWarningClass(true), hidden);
- element.loading = false;
- assert.equal(element.computeNewlineWarningClass(false), hidden);
- assert.equal(element.computeNewlineWarningClass(true), shown);
- });
});
suite('key locations', () => {
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index a9f88bd..da08a1f 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -8,9 +8,11 @@
import {DiffFileMetaInfo, DiffInfo} from '../../../types/diff';
import {DiffLayer, DiffLayerListener} from '../../../types/types';
import {Side} from '../../../constants/constants';
-import {getAppContext} from '../../../services/app-context';
import {SyntaxLayerLine} from '../../../types/syntax-worker-api';
-import {CancelablePromise, makeCancelable} from '../../../scripts/util';
+import {CancelablePromise, makeCancelable} from '../../../utils/async-util';
+import {HighlightService} from '../../../services/highlight/highlight-service';
+import {Provider} from '../../../models/dependency';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
const LANGUAGE_MAP = new Map<string, string>([
['application/dart', 'dart'],
@@ -162,9 +164,10 @@
private listeners: DiffLayerListener[] = [];
- private readonly highlightService = getAppContext().highlightService;
-
- private readonly reportingService = getAppContext().reportingService;
+ constructor(
+ private readonly getHighlightService: Provider<HighlightService>,
+ private readonly getReportingService: Provider<ReportingService>
+ ) {}
setEnabled(enabled: boolean) {
this.enabled = enabled;
@@ -276,7 +279,7 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (!err.isCanceled)
- this.reportingService.error('Diff Syntax Layer', err as Error);
+ this.getReportingService().error('Diff Syntax Layer', err as Error);
// One source of "error" can promise cancelation.
this.leftRanges = [];
this.rightRanges = [];
@@ -287,7 +290,7 @@
language?: string,
code?: string
): CancelablePromise<SyntaxLayerLine[]> {
- const hlPromise = this.highlightService.highlight(language, code);
+ const hlPromise = this.getHighlightService().highlight(language, code);
return makeCancelable(hlPromise);
}
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
index 5c9a6cc..c6c46f9 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
@@ -5,8 +5,14 @@
*/
import {assert} from '@open-wc/testing';
import {DiffInfo, GrDiffLineType, Side} from '../../../api/diff';
+import {getAppContext} from '../../../services/app-context';
+import {
+ HighlightService,
+ highlightServiceToken,
+} from '../../../services/highlight/highlight-service';
import '../../../test/common-test-setup';
-import {mockPromise, stubHighlightService} from '../../../test/test-utils';
+import {testResolver} from '../../../test/common-test-setup';
+import {mockPromise} from '../../../test/test-utils';
import {SyntaxLayerLine} from '../../../types/syntax-worker-api';
import {GrDiffLine} from '../gr-diff/gr-diff-line';
import {GrSyntaxLayerWorker} from './gr-syntax-layer-worker';
@@ -62,6 +68,7 @@
suite('gr-syntax-layer-worker tests', () => {
let layer: GrSyntaxLayerWorker;
let listener: sinon.SinonStub;
+ let highlightService: HighlightService;
const annotate = (side: Side, lineNumber: number, text: string) => {
const el = document.createElement('div');
@@ -76,7 +83,11 @@
};
setup(() => {
- layer = new GrSyntaxLayerWorker();
+ highlightService = testResolver(highlightServiceToken);
+ layer = new GrSyntaxLayerWorker(
+ () => highlightService,
+ () => getAppContext().reportingService
+ );
});
test('cancel processing', async () => {
@@ -84,7 +95,7 @@
const mockPromise2 = mockPromise<SyntaxLayerLine[]>();
const mockPromise3 = mockPromise<SyntaxLayerLine[]>();
const mockPromise4 = mockPromise<SyntaxLayerLine[]>();
- const stub = stubHighlightService('highlight');
+ const stub = sinon.stub(highlightService, 'highlight');
stub.onCall(0).returns(mockPromise1);
stub.onCall(1).returns(mockPromise2);
stub.onCall(2).returns(mockPromise3);
@@ -116,7 +127,7 @@
setup(() => {
listener = sinon.stub();
layer.addListener(listener);
- stubHighlightService('highlight').callsFake((lang?: string) => {
+ sinon.stub(highlightService, 'highlight').callsFake((lang?: string) => {
if (lang === 'lang-left') return Promise.resolve(leftRanges);
if (lang === 'lang-right') return Promise.resolve(rightRanges);
return Promise.resolve([]);
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index f865d6d..36ebb9f 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -63,30 +63,6 @@
restApiService: (_ctx: Partial<AppContext>) => {
throw new Error('restApiService is not implemented');
},
- jsApiService: (_ctx: Partial<AppContext>) => {
- throw new Error('jsApiService is not implemented');
- },
- storageService: (_ctx: Partial<AppContext>) => {
- throw new Error('storageService is not implemented');
- },
- userModel: (_ctx: Partial<AppContext>) => {
- throw new Error('userModel is not implemented');
- },
- accountsModel: (_ctx: Partial<AppContext>) => {
- throw new Error('accountsModel is not implemented');
- },
- routerModel: (_ctx: Partial<AppContext>) => {
- throw new Error('routerModel is not implemented');
- },
- shortcutsService: (_ctx: Partial<AppContext>) => {
- throw new Error('shortcutsService is not implemented');
- },
- pluginsModel: (_ctx: Partial<AppContext>) => {
- throw new Error('pluginsModel is not implemented');
- },
- highlightService: (_ctx: Partial<AppContext>) => {
- throw new Error('highlightService is not implemented');
- },
};
return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
index 2e0e315..99e2bac 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -3,7 +3,6 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {getRootElement} from '../../scripts/rootElement';
import {Constructor} from '../../utils/common-util';
import {LitElement, PropertyValues} from 'lit';
import {property, query} from 'lit/decorators.js';
@@ -48,21 +47,6 @@
focusEvent?: FocusEvent;
}
-export function getHovercardContainer(
- options: {createIfNotExists: boolean} = {createIfNotExists: false}
-): HTMLElement | null {
- let container = getRootElement().querySelector<HTMLElement>(
- `#${containerId}`
- );
- if (!container && options.createIfNotExists) {
- // If it does not exist, create and initialize the hovercard container.
- container = document.createElement('div');
- container.setAttribute('id', containerId);
- getRootElement().appendChild(container);
- }
- return container;
-}
-
/**
* How long should we wait before showing the hovercard when the user hovers
* over the element?
@@ -177,7 +161,7 @@
this.addTargetEventListeners();
}
- this.container = getHovercardContainer({createIfNotExists: true});
+ this.container = this.getContainer();
this.cleanups.push(
addShortcut(
this,
@@ -334,6 +318,29 @@
);
}
+ getHost(): HTMLElement {
+ let el = this._target as Node;
+ while (el) {
+ if ((el as HTMLElement).tagName === 'DIALOG') {
+ return el as HTMLElement;
+ }
+ el = el.parentNode || (el as ShadowRoot).host;
+ }
+ return document.body;
+ }
+
+ getContainer(): HTMLElement | null {
+ const host = this.getHost();
+ let container = host.querySelector<HTMLElement>(`#${containerId}`);
+ if (!container) {
+ // If it does not exist, create and initialize the hovercard container.
+ container = document.createElement('div');
+ container.setAttribute('id', containerId);
+ host.appendChild(container);
+ }
+ return container;
+ }
+
/**
* Returns the target element that the hovercard is anchored to (the `id` of
* the `for` property).
@@ -541,16 +548,16 @@
this.updatePositionTo(position);
if (this._isInsideViewport()) return;
}
- console.warn('Could not find a visible position for the hovercard.');
+ this.updatePositionTo(this.position);
}
_isInsideViewport() {
const thisRect = this.getBoundingClientRect();
- if (thisRect.top < 0) return false;
- if (thisRect.left < 0) return false;
- const docuRect = document.documentElement.getBoundingClientRect();
- if (thisRect.bottom > docuRect.height) return false;
- if (thisRect.right > docuRect.width) return false;
+ const hostRect = this.getHost().getBoundingClientRect();
+ if (thisRect.top < hostRect.top) return false;
+ if (thisRect.left < hostRect.left) return false;
+ if (thisRect.bottom > hostRect.bottom) return false;
+ if (thisRect.right > hostRect.right) return false;
return true;
}
@@ -575,12 +582,12 @@
// in the width and height of the bounding client rect.
this.style.cssText = '';
- const docuRect = document.documentElement.getBoundingClientRect();
+ const hostRect = this.getHost().getBoundingClientRect();
const targetRect = this._target.getBoundingClientRect();
const thisRect = this.getBoundingClientRect();
- const targetLeft = targetRect.left - docuRect.left;
- const targetTop = targetRect.top - docuRect.top;
+ const targetLeft = targetRect.left - hostRect.left;
+ const targetTop = targetRect.top - hostRect.top;
let hovercardLeft;
let hovercardTop;
@@ -639,6 +646,7 @@
// Used for tests
mouseHide(e: MouseEvent): void;
+ getHost(): HTMLElement;
hide(props: MouseKeyboardOrFocusEvent): void;
container: HTMLElement | null;
hideTask?: DelayedTask;
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
index 8d32c5b9..ffae9e5 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -73,7 +73,7 @@
assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
- const parentRect = document.documentElement.getBoundingClientRect();
+ const parentRect = element.getHost().getBoundingClientRect();
const targetRect = element._target!.getBoundingClientRect();
const thisRect = element.getBoundingClientRect();
@@ -93,6 +93,16 @@
);
});
+ test('getHost', () => {
+ element._target = document.createElement('span');
+
+ const dialog = document.createElement('dialog');
+
+ assert.deepEqual(element.getHost(), document.body);
+ dialog.appendChild(element._target);
+ assert.deepEqual(element.getHost(), dialog);
+ });
+
test('hide', () => {
element.mouseHide(new MouseEvent('click'));
const style = getComputedStyle(element);
diff --git a/polygerrit-ui/app/models/accounts-model/accounts-model.ts b/polygerrit-ui/app/models/accounts-model/accounts-model.ts
index 3f35127..2bf6068 100644
--- a/polygerrit-ui/app/models/accounts-model/accounts-model.ts
+++ b/polygerrit-ui/app/models/accounts-model/accounts-model.ts
@@ -6,7 +6,6 @@
import {AccountDetailInfo, AccountInfo} from '../../api/rest-api';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../services/registry';
import {UserId} from '../../types/common';
import {getUserId, isDetailedAccount} from '../../utils/account-util';
import {define} from '../dependency';
@@ -18,7 +17,7 @@
export const accountsModelToken = define<AccountsModel>('accounts-model');
-export class AccountsModel extends Model<AccountsState> implements Finalizable {
+export class AccountsModel extends Model<AccountsState> {
constructor(readonly restApiService: RestApiService) {
super({
accounts: {},
diff --git a/polygerrit-ui/app/models/browser/browser-model.ts b/polygerrit-ui/app/models/browser/browser-model.ts
index 1592cd8..50b6325 100644
--- a/polygerrit-ui/app/models/browser/browser-model.ts
+++ b/polygerrit-ui/app/models/browser/browser-model.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable, combineLatest} from 'rxjs';
-import {Finalizable} from '../../services/registry';
import {define} from '../dependency';
import {DiffViewMode} from '../../api/diff';
import {UserModel} from '../user/user-model';
@@ -26,7 +25,7 @@
export const browserModelToken = define<BrowserModel>('browser-model');
-export class BrowserModel extends Model<BrowserState> implements Finalizable {
+export class BrowserModel extends Model<BrowserState> {
private readonly isScreenTooSmall$ = select(
this.state$,
state =>
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index f706712..b13a16f 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -14,7 +14,6 @@
Hashtag,
} from '../../api/rest-api';
import {Model} from '../model';
-import {Finalizable} from '../../services/registry';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {define} from '../dependency';
import {select} from '../../utils/observable-util';
@@ -50,10 +49,7 @@
allChanges: new Map(),
};
-export class BulkActionsModel
- extends Model<BulkActionsState>
- implements Finalizable
-{
+export class BulkActionsModel extends Model<BulkActionsState> {
constructor(private readonly restApiService: RestApiService) {
super(initialState);
}
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 12d09b3..8282a3f 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -32,7 +32,6 @@
import {ChangeInfo} from '../../types/common';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
import {assertIsDefined} from '../../utils/common-util';
import {Model} from '../model';
@@ -148,7 +147,7 @@
export const changeModelToken = define<ChangeModel>('change-model');
-export class ChangeModel extends Model<ChangeState> implements Finalizable {
+export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
private patchNum?: PatchSetNum;
@@ -258,9 +257,9 @@
);
constructor(
- readonly routerModel: RouterModel,
- readonly restApiService: RestApiService,
- readonly userModel: UserModel
+ private readonly routerModel: RouterModel,
+ private readonly restApiService: RestApiService,
+ private readonly userModel: UserModel
) {
super(initialState);
this.subscriptions = [
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index fdf9e04..a2fc7c9 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -28,10 +28,12 @@
} from '../../types/common';
import {ParsedChangeInfo} from '../../types/types';
import {getAppContext} from '../../services/app-context';
-import {GerritView} from '../../services/router/router-model';
+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';
suite('updateChangeWithEdit() tests', () => {
test('undefined change', async () => {
@@ -81,9 +83,9 @@
setup(() => {
changeModel = new ChangeModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
getAppContext().restApiService,
- getAppContext().userModel
+ testResolver(userModelToken)
);
knownChange = {
...createChange(),
@@ -119,7 +121,7 @@
assert.equal(stub.callCount, 0);
assert.isUndefined(state?.change);
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -138,7 +140,7 @@
const promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -162,7 +164,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -176,7 +178,7 @@
_number: 123 as NumericChangeId,
};
promise = mockPromise<ParsedChangeInfo | undefined>();
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: otherChange._number,
});
@@ -195,7 +197,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -206,7 +208,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(undefined);
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: undefined,
});
@@ -218,7 +220,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(knownChange);
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -285,6 +287,10 @@
// And the missing `replay` led to a bug that was hard to find. That is why
// we are testing this explicitly here.
test('basePatchNum$ selector', async () => {
+ // Let's first wait for the selector to emit. Then we can test the replay
+ // below.
+ await waitUntilObserved(changeModel.basePatchNum$, x => x === PARENT);
+
const spy = sinon.spy();
changeModel.basePatchNum$.subscribe(spy);
@@ -293,7 +299,9 @@
assert.equal(spy.lastCall.firstArg, PARENT);
// test update
- changeModel.routerModel.updateState({basePatchNum: 1 as PatchSetNumber});
+ testResolver(routerModelToken).updateState({
+ basePatchNum: 1 as PatchSetNumber,
+ });
assert.equal(spy.callCount, 2);
assert.equal(spy.lastCall.firstArg, 1 as PatchSetNumber);
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 6922f6d..07e64a2 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -15,7 +15,6 @@
import {combineLatest, of, from} from 'rxjs';
import {switchMap, map} from 'rxjs/operators';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
import {FileInfoStatus, SpecialFilePath} from '../../constants/constants';
import {specialFilePathCompare} from '../../utils/path-list-util';
@@ -113,7 +112,7 @@
export const filesModelToken = define<FilesModel>('files-model');
-export class FilesModel extends Model<FilesState> implements Finalizable {
+export class FilesModel extends Model<FilesState> {
public readonly files$ = select(this.state$, state => state.files);
public readonly filesWithUnmodified$ = select(
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 6b35056..4715abf 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -12,7 +12,6 @@
} from './checks-util';
import {assertIsDefined} from '../../utils/common-util';
import {select} from '../../utils/observable-util';
-import {Finalizable} from '../../services/registry';
import {
BehaviorSubject,
combineLatest,
@@ -53,7 +52,6 @@
import {ReportingService} from '../../services/gr-reporting/gr-reporting';
import {Execution, Interaction, Timing} from '../../constants/reporting';
import {fireAlert, fireEvent} from '../../utils/event-util';
-import {RouterModel} from '../../services/router/router-model';
import {Model} from '../model';
import {define} from '../dependency';
import {
@@ -182,7 +180,7 @@
[name: string]: string;
}
-export class ChecksModel extends Model<ChecksState> implements Finalizable {
+export class ChecksModel extends Model<ChecksState> {
private readonly providers: {[name: string]: ChecksProvider} = {};
private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
@@ -374,11 +372,10 @@
);
constructor(
- readonly routerModel: RouterModel,
- readonly changeViewModel: ChangeViewModel,
- readonly changeModel: ChangeModel,
- readonly reporting: ReportingService,
- readonly pluginsModel: PluginsModel
+ private readonly changeViewModel: ChangeViewModel,
+ private readonly changeModel: ChangeModel,
+ private readonly reporting: ReportingService,
+ private readonly pluginsModel: PluginsModel
) {
super({
pluginStateLatest: {},
@@ -637,8 +634,15 @@
}
updateStateSetPatchset(num?: PatchSetNumber) {
+ const newPatchset = num === this.latestPatchNum ? undefined : num;
+ const oldPatchset = this.changeViewModel.getState()?.checksPatchset;
+ // For `checksPatchset` itself we could just let updateState() do the
+ // standard old===new comparison. But we have to make sure here that
+ // the attempt reset only actually happens when a new patchset is chosen.
+ if (newPatchset === oldPatchset) return;
this.changeViewModel.updateState({
- checksPatchset: num === this.latestPatchNum ? undefined : num,
+ checksPatchset: newPatchset,
+ attempt: LATEST_ATTEMPT,
});
}
@@ -753,15 +757,14 @@
patchset === ChecksPatchset.LATEST
? this.changeModel.latestPatchNum$
: this.checksSelectedPatchsetNumber$,
- this.reloadSubjects[pluginName].pipe(
- throttleTime(1000, undefined, {trailing: true, leading: true})
- ),
+ this.reloadSubjects[pluginName],
pollIntervalMs === 0 ? from([0]) : timer(0, pollIntervalMs),
this.documentVisibilityChange$,
])
.pipe(
takeWhile(_ => !!this.providers[pluginName]),
filter(_ => document.visibilityState !== 'hidden'),
+ throttleTime(500, undefined, {leading: true, trailing: true}),
switchMap(([change, patchNum]): Observable<FetchResponse> => {
if (!change || !patchNum) return of(this.empty());
if (typeof patchNum !== 'number') return of(this.empty());
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index 3489c5a..da4e3f1 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -24,6 +24,7 @@
import {testResolver} from '../../test/common-test-setup';
import {changeViewModelToken} from '../views/change';
import {NumericChangeId, PatchSetNumber} from '../../api/rest-api';
+import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
const PLUGIN_NAME = 'test-plugin';
@@ -69,11 +70,10 @@
setup(() => {
model = new ChecksModel(
- getAppContext().routerModel,
testResolver(changeViewModelToken),
testResolver(changeModelToken),
getAppContext().reportingService,
- getAppContext().pluginsModel
+ testResolver(pluginLoaderToken).pluginsModel
);
model.checksLatest$.subscribe(c => (current = c[PLUGIN_NAME]));
});
@@ -84,7 +84,7 @@
test('register and fetch', async () => {
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -96,7 +96,7 @@
await waitUntil(() => change === undefined);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
await waitUntilCalled(fetchSpy, 'fetch');
@@ -108,10 +108,10 @@
assert.equal(model.changeNum, testChange._number);
});
- test('reload throttle', async () => {
+ test('fetch throttle', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -123,18 +123,33 @@
await waitUntil(() => change === undefined);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
- clock.tick(1);
+
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+
+ // Does not emit at 'leading' of throttle interval,
+ // because fetch() is not called when change is undefined.
+ assert.equal(fetchSpy.callCount, 0);
+
+ // 600 ms is greater than the 500 ms throttle time.
+ clock.tick(600);
+ // emits at 'trailing' of throttle interval
assert.equal(fetchSpy.callCount, 1);
- // The second reload call will be processed, but only after a 1s throttle.
model.reload('test-plugin');
- clock.tick(100);
- assert.equal(fetchSpy.callCount, 1);
- // 2000 ms is greater than the 1000 ms throttle time.
- clock.tick(2000);
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+ // emits at 'leading' of throttle interval
assert.equal(fetchSpy.callCount, 2);
+
+ // 600 ms is greater than the 500 ms throttle time.
+ clock.tick(600);
+ // emits at 'trailing' of throttle interval
+ assert.equal(fetchSpy.callCount, 3);
});
test('triggerAction', async () => {
@@ -268,7 +283,7 @@
test('polls for changes', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -280,10 +295,10 @@
await waitUntil(() => change === undefined);
clock.tick(1);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
+ clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
- clock.tick(1);
const pollCount = fetchSpy.callCount;
// polling should continue while we wait
@@ -295,7 +310,7 @@
test('does not poll when config specifies 0 seconds', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -307,8 +322,9 @@
await waitUntil(() => change === undefined);
clock.tick(1);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
+ clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
clock.tick(1);
const pollCount = fetchSpy.callCount;
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 7ccdf91..6a5933c 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -17,7 +17,7 @@
import {PatchSetNumber} from '../../api/rest-api';
import {FixSuggestionInfo, FixReplacementInfo} from '../../types/common';
import {OpenFixPreviewEventDetail} from '../../types/events';
-import {notUndefined} from '../../types/types';
+import {isDefined} from '../../types/types';
import {PROVIDED_FIX_ID} from '../../utils/comment-util';
import {assert, assertNever} from '../../utils/common-util';
import {fire} from '../../utils/event-util';
@@ -94,7 +94,7 @@
if (!result?.fixes) return;
const fixSuggestions = result.fixes
.map(f => rectifyFix(f, result?.checkName))
- .filter(notUndefined);
+ .filter(isDefined);
if (fixSuggestions.length === 0) return;
const eventDetail: OpenFixPreviewEventDetail = {
patchNum: result.patchset as PatchSetNumber,
@@ -116,7 +116,7 @@
if (!fix?.replacements) return undefined;
const replacements = fix.replacements
.map(rectifyReplacement)
- .filter(notUndefined);
+ .filter(isDefined);
if (replacements.length === 0) return undefined;
return {
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index b0ad417..a7b43ca 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -28,7 +28,6 @@
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
import {RouterModel} from '../../services/router/router-model';
-import {Finalizable} from '../../services/registry';
import {define} from '../dependency';
import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
import {fire, fireAlert, fireEvent} from '../../utils/event-util';
@@ -52,7 +51,7 @@
shareReplay,
switchMap,
} from 'rxjs/operators';
-import {notUndefined} from '../../types/types';
+import {isDefined} from '../../types/types';
export interface CommentState {
/** undefined means 'still loading' */
@@ -225,7 +224,7 @@
}
export const commentsModelToken = define<CommentsModel>('comments-model');
-export class CommentsModel extends Model<CommentState> implements Finalizable {
+export class CommentsModel extends Model<CommentState> {
public readonly commentsLoading$ = select(
this.state$,
commentState =>
@@ -299,7 +298,7 @@
uniqueUsers.map(user => from(this.accountsModel.fillDetails(user)));
return forkJoin(filledUsers$);
}),
- map(users => users.filter(notUndefined)),
+ map(users => users.filter(isDefined)),
distinctUntilChanged(deepEqual),
shareReplay(1)
);
@@ -326,7 +325,7 @@
uniqueUsers.map(user => from(this.accountsModel.fillDetails(user)));
return forkJoin(filledUsers$);
}),
- map(users => users.filter(notUndefined)),
+ map(users => users.filter(isDefined)),
distinctUntilChanged(deepEqual),
shareReplay(1)
);
@@ -385,11 +384,11 @@
private discardedDrafts: DraftInfo[] = [];
constructor(
- readonly routerModel: RouterModel,
- readonly changeModel: ChangeModel,
- readonly accountsModel: AccountsModel,
- readonly restApiService: RestApiService,
- readonly reporting: ReportingService
+ private readonly routerModel: RouterModel,
+ private readonly changeModel: ChangeModel,
+ private readonly accountsModel: AccountsModel,
+ private readonly restApiService: RestApiService,
+ private readonly reporting: ReportingService
) {
super(initialState);
this.subscriptions.push(
diff --git a/polygerrit-ui/app/models/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
index 32ea1bc..4db5d57 100644
--- a/polygerrit-ui/app/models/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -23,11 +23,12 @@
} from '../../test/test-data-generators';
import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
import {getAppContext} from '../../services/app-context';
-import {GerritView} from '../../services/router/router-model';
+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';
suite('comments model tests', () => {
test('updateStateDeleteDraft', () => {
@@ -69,9 +70,9 @@
test('loads comments', async () => {
const model = new CommentsModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
testResolver(changeModelToken),
- getAppContext().accountsModel,
+ testResolver(accountsModelToken),
getAppContext().restApiService,
getAppContext().reportingService
);
@@ -97,11 +98,11 @@
model.portedComments$.subscribe(c => (portedComments = c ?? {}))
);
- model.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: TEST_NUMERIC_CHANGE_ID,
});
- model.changeModel.updateStateChange(createParsedChange());
+ testResolver(changeModelToken).updateStateChange(createParsedChange());
await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
await waitUntilCalled(diffRobotCommentsSpy, 'diffRobotCommentsSpy');
@@ -130,9 +131,9 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
testResolver(changeModelToken),
- getAppContext().accountsModel,
+ testResolver(accountsModelToken),
getAppContext().restApiService,
getAppContext().reportingService
);
@@ -158,9 +159,9 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
testResolver(changeModelToken),
- getAppContext().accountsModel,
+ testResolver(accountsModelToken),
getAppContext().restApiService,
getAppContext().reportingService
);
diff --git a/polygerrit-ui/app/models/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
index 6e374d1..4c9bb35c 100644
--- a/polygerrit-ui/app/models/config/config-model.ts
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -6,7 +6,6 @@
import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
import {from, of} from 'rxjs';
import {switchMap} from 'rxjs/operators';
-import {Finalizable} from '../../services/registry';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {ChangeModel} from '../change/change-model';
import {select} from '../../utils/observable-util';
@@ -20,7 +19,7 @@
}
export const configModelToken = define<ConfigModel>('config-model');
-export class ConfigModel extends Model<ConfigState> implements Finalizable {
+export class ConfigModel extends Model<ConfigState> {
public repoConfig$ = select(
this.state$,
configState => configState.repoConfig
diff --git a/polygerrit-ui/app/models/dependency.ts b/polygerrit-ui/app/models/dependency.ts
index 5499db2..3b5081a 100644
--- a/polygerrit-ui/app/models/dependency.ts
+++ b/polygerrit-ui/app/models/dependency.ts
@@ -47,14 +47,15 @@
* ---
*
* Ancestor components will inject the dependencies that a child component
- * requires by providing factories for those values.
+ * requires by providing providers for those values.
*
*
* To provide a dependency, a component needs to specify the following prior
* to finishing its connectedCallback:
*
* ```
- * provide(this, fooToken, () => new FooImpl())
+ * const fooImpl = new FooImpl();
+ * provide(this, fooToken, () => fooImpl);
* ```
* Dependencies are injected as factories in case the construction of them
* depends on other dependencies further up the component chain. For instance,
@@ -63,7 +64,8 @@
*
* ```
* const barRef = resolve(this, barToken);
- * provide(this, fooToken, () => new FooImpl(barRef()));
+ * const fooImpl = new FooImpl(barRef());
+ * provide(this, fooToken, () => fooImpl);
* ```
*
* Lifetime guarantees
@@ -188,7 +190,7 @@
*/
export interface DependencyRequest<T> {
readonly dependency: DependencyToken<T>;
- readonly callback: Callback<T>;
+ readonly callback: Callback<Provider<T>>;
}
declare global {
@@ -218,7 +220,7 @@
{
public constructor(
public readonly dependency: DependencyToken<T>,
- public readonly callback: Callback<T>
+ public readonly callback: Callback<Provider<T>>
) {
super('request-dependency', {bubbles: true, composed: true});
}
@@ -238,12 +240,20 @@
}
}
+function makeDependencyError<T>(
+ host: HTMLElement,
+ dependency: DependencyToken<T>
+): DependencyError<T> {
+ const dep = dependency.description;
+ const tag = host.tagName;
+ const msg = `Could not resolve dependency '${dep}' in '${tag}'`;
+ return new DependencyError(dependency, msg);
+}
+
class DependencySubscriber<T>
implements ReactiveController, ResolvedDependency<T>
{
- private value?: T;
-
- private resolved = false;
+ private provider?: Provider<T>;
constructor(
private readonly host: ReactiveControllerHost & HTMLElement,
@@ -251,34 +261,26 @@
) {}
get() {
- this.checkResolved();
- return this.value!;
+ if (!this.provider) {
+ throw makeDependencyError(this.host, this.dependency);
+ }
+ return this.provider();
}
hostConnected() {
- this.value = undefined;
- this.resolved = false;
+ this.provider = undefined;
this.host.dispatchEvent(
- new DependencyRequestEvent(this.dependency, (value: T) => {
- this.resolved = true;
- this.value = value;
+ new DependencyRequestEvent(this.dependency, (provider: Provider<T>) => {
+ this.provider = provider;
})
);
- this.checkResolved();
- }
-
- checkResolved() {
- if (this.resolved) return;
- const dep = this.dependency.description;
- const tag = this.host.tagName;
- const msg = `Could not resolve dependency '${dep}' in '${tag}'`;
- throw new DependencyError(this.dependency, msg);
+ if (!this.provider) {
+ throw makeDependencyError(this.host, this.dependency);
+ }
}
}
class DependencyProvider<T> implements ReactiveController {
- private value?: T;
-
constructor(
private readonly host: ReactiveControllerHost & HTMLElement,
private readonly dependency: DependencyToken<T>,
@@ -286,20 +288,17 @@
) {}
hostConnected() {
- // Delay construction in case the provider has its own dependencies.
- this.value = this.provider();
this.host.addEventListener('request-dependency', this.fullfill);
}
hostDisconnected() {
this.host.removeEventListener('request-dependency', this.fullfill);
- this.value = undefined;
}
private readonly fullfill = (ev: DependencyRequestEvent<unknown>) => {
if (ev.dependency !== this.dependency) return;
ev.stopPropagation();
ev.preventDefault();
- ev.callback(this.value!);
+ ev.callback(this.provider);
};
}
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 7826c45..ccca0eb 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -3,7 +3,6 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {Finalizable} from '../../services/registry';
import {Observable, Subject} from 'rxjs';
import {
CheckResult,
@@ -12,7 +11,6 @@
ChecksProvider,
} from '../../api/checks';
import {Model} from '../model';
-import {define} from '../dependency';
import {select} from '../../utils/observable-util';
export interface ChecksPlugin {
@@ -35,9 +33,7 @@
checksPlugins: ChecksPlugin[];
}
-export const pluginsModelToken = define<PluginsModel>('plugins-model');
-
-export class PluginsModel extends Model<PluginsState> implements Finalizable {
+export class PluginsModel extends Model<PluginsState> {
/** Private version of the event bus below. */
private checksAnnounceSubject$ = new Subject<ChecksPlugin>();
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index fa00a0b..97f90fa 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {from, of, Observable} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
+import {filter, switchMap} from 'rxjs/operators';
import {
DiffPreferencesInfo as DiffPreferencesInfoAPI,
DiffViewMode,
@@ -23,27 +23,67 @@
} from '../../constants/constants';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {DiffPreferencesInfo} from '../../types/diff';
-import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
+import {define} from '../dependency';
import {Model} from '../model';
+import {isDefined} from '../../types/types';
export interface UserState {
/**
* Keeps being defined even when credentials have expired.
+ *
+ * `undefined` can mean that the app is still starting up and we have not
+ * tried loading an account object yet. If you want to wait until the
+ * `account` is known, then use `accountLoaded` below.
*/
account?: AccountDetailInfo;
- preferences: PreferencesInfo;
- diffPreferences: DiffPreferencesInfo;
- editPreferences: EditPreferencesInfo;
+ /**
+ * Starts as `false` and switches to `true` after the first `getAccount` call.
+ * A common use case for this is to wait with loading or doing something until
+ * we know whether the user is logged in or not, see `loadedAccount$` below.
+ *
+ * This value cannot change back to `false` once it has become `true`.
+ *
+ * This value does *not* indicate whether the user is logged in or whether an
+ * `account` object is available. If the first `getAccount()` call returns
+ * `undefined`, then `accountLoaded` still becomes true, even if `account`
+ * stays `undefined`.
+ */
+ accountLoaded: boolean;
+ preferences?: PreferencesInfo;
+ diffPreferences?: DiffPreferencesInfo;
+ editPreferences?: EditPreferencesInfo;
capabilities?: AccountCapabilityInfo;
}
-export class UserModel extends Model<UserState> implements Finalizable {
+export const userModelToken = define<UserModel>('user-model');
+
+export class UserModel extends Model<UserState> {
+ /**
+ * Note that the initially emitted `undefined` value can mean "not loaded
+ * the account into object yet" or "user is not logged in". Consider using
+ * `loadedAccount$` below.
+ *
+ * TODO: Maybe consider changing all usages to `loadedAccount$`.
+ */
readonly account$: Observable<AccountDetailInfo | undefined> = select(
this.state$,
userState => userState.account
);
+ /**
+ * Only emits once we have tried to actually load the account. Note that
+ * this does not initially emit a value.
+ *
+ * So if this emits `undefined`, then you actually know that the user is not
+ * logged in. And for logged in users you will never get an initial
+ * `undefined` emission.
+ */
+ readonly loadedAccount$: Observable<AccountDetailInfo | undefined> = select(
+ this.state$.pipe(filter(s => s.accountLoaded)),
+ userState => userState.account
+ );
+
/** Note that this may still be true, even if credentials have expired. */
readonly loggedIn$: Observable<boolean> = select(
this.account$,
@@ -61,17 +101,17 @@
readonly preferences$: Observable<PreferencesInfo> = select(
this.state$,
userState => userState.preferences
- );
+ ).pipe(filter(isDefined));
readonly diffPreferences$: Observable<DiffPreferencesInfo> = select(
this.state$,
userState => userState.diffPreferences
- );
+ ).pipe(filter(isDefined));
readonly editPreferences$: Observable<EditPreferencesInfo> = select(
this.state$,
userState => userState.editPreferences
- );
+ ).pipe(filter(isDefined));
readonly preferenceDiffViewMode$: Observable<DiffViewMode> = select(
this.preferences$,
@@ -83,11 +123,14 @@
preference => preference.theme
);
+ readonly preferenceChangesPerPage$: Observable<number> = select(
+ this.preferences$,
+ preference => preference.changes_per_page
+ );
+
constructor(readonly restApiService: RestApiService) {
super({
- preferences: createDefaultPreferences(),
- diffPreferences: createDefaultDiffPrefs(),
- editPreferences: createDefaultEditPrefs(),
+ accountLoaded: false,
});
this.subscriptions = [
from(this.restApiService.getAccount()).subscribe(
@@ -95,7 +138,7 @@
this.setAccount(account);
}
),
- this.account$
+ this.loadedAccount$
.pipe(
switchMap(account => {
if (!account) return of(createDefaultPreferences());
@@ -105,7 +148,7 @@
.subscribe((preferences?: PreferencesInfo) => {
this.setPreferences(preferences ?? createDefaultPreferences());
}),
- this.account$
+ this.loadedAccount$
.pipe(
switchMap(account => {
if (!account) return of(createDefaultDiffPrefs());
@@ -115,7 +158,7 @@
.subscribe((diffPrefs?: DiffPreferencesInfoAPI) => {
this.setDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
}),
- this.account$
+ this.loadedAccount$
.pipe(
switchMap(account => {
if (!account) return of(createDefaultEditPrefs());
@@ -125,7 +168,7 @@
.subscribe((editPrefs?: EditPreferencesInfo) => {
this.setEditPreferences(editPrefs ?? createDefaultEditPrefs());
}),
- this.account$
+ this.loadedAccount$
.pipe(
switchMap(account => {
if (!account) return of(undefined);
@@ -196,6 +239,6 @@
}
setAccount(account?: AccountDetailInfo) {
- this.updateState({account});
+ this.updateState({account, accountLoaded: true});
}
}
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 100c46b..31d511a 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -30,7 +30,7 @@
view: GerritView.CHANGE;
changeNum: NumericChangeId;
- project: RepoName;
+ repo: RepoName;
edit?: boolean;
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
@@ -70,7 +70,7 @@
*/
export type CreateChangeUrlObject = Omit<
ChangeViewState,
- 'view' | 'changeNum' | 'project'
+ 'view' | 'changeNum' | 'repo'
> & {
change: Pick<ChangeInfo, '_number' | 'project'>;
};
@@ -89,7 +89,7 @@
...obj,
view: GerritView.CHANGE,
changeNum: obj.change._number,
- project: obj.change.project,
+ repo: obj.change.project,
};
}
return {...obj, view: GerritView.CHANGE};
@@ -144,8 +144,8 @@
if (state.messageHash) {
suffix += state.messageHash;
}
- if (state.project) {
- const encodedProject = encodeURL(state.project, true);
+ if (state.repo) {
+ const encodedProject = encodeURL(state.repo, true);
return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
} else {
return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index 24ced82..b34a1ba 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -17,7 +17,7 @@
const STATE: ChangeViewState = {
view: GerritView.CHANGE,
changeNum: 1234 as NumericChangeId,
- project: 'test' as RepoName,
+ repo: 'test' as RepoName,
};
suite('change view state tests', () => {
@@ -71,7 +71,7 @@
const state: ChangeViewState = {
view: GerritView.CHANGE,
changeNum: 1234 as NumericChangeId,
- project: 'x+/y+/z+/w' as RepoName,
+ repo: 'x+/y+/z+/w' as RepoName,
};
assert.equal(createChangeUrl(state), '/c/x%252B/y%252B/z%252B/w/+/1234');
});
diff --git a/polygerrit-ui/app/models/views/diff.ts b/polygerrit-ui/app/models/views/diff.ts
index 3cc107a..34f4ee7 100644
--- a/polygerrit-ui/app/models/views/diff.ts
+++ b/polygerrit-ui/app/models/views/diff.ts
@@ -24,7 +24,7 @@
export interface DiffViewState extends ViewState {
view: GerritView.DIFF;
changeNum: NumericChangeId;
- project?: RepoName;
+ repo?: RepoName;
commentId?: UrlEncodedCommentId;
path?: string;
patchNum?: RevisionPatchSetNum;
@@ -60,7 +60,7 @@
...obj,
view: GerritView.DIFF,
changeNum: obj.change._number,
- project: obj.change.project,
+ repo: obj.change.project,
};
}
return {...obj, view: GerritView.DIFF};
@@ -87,8 +87,8 @@
suffix = `/comment/${state.commentId}` + suffix;
}
- if (state.project) {
- const encodedProject = encodeURL(state.project, true);
+ if (state.repo) {
+ const encodedProject = encodeURL(state.repo, true);
return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
} else {
return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
diff --git a/polygerrit-ui/app/models/views/diff_test.ts b/polygerrit-ui/app/models/views/diff_test.ts
index b0f91bb..7fab2a4 100644
--- a/polygerrit-ui/app/models/views/diff_test.ts
+++ b/polygerrit-ui/app/models/views/diff_test.ts
@@ -21,7 +21,7 @@
changeNum: 42 as NumericChangeId,
path: 'x+y/path.cpp' as RepoName,
patchNum: 12 as RevisionPatchSetNum,
- project: '' as RepoName,
+ repo: '' as RepoName,
};
assert.equal(createDiffUrl(params), '/c/42/12/x%252By/path.cpp');
@@ -29,7 +29,7 @@
assert.equal(createDiffUrl(params).substring(0, 5), '/base');
window.CANONICAL_PATH = undefined;
- params.project = 'test' as RepoName;
+ params.repo = 'test' as RepoName;
assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
params.basePatchNum = 6 as BasePatchSetNum;
@@ -57,7 +57,7 @@
changeNum: 42 as NumericChangeId,
path: 'x+y/path.cpp',
patchNum: 12 as RevisionPatchSetNum,
- project: 'x+/y' as RepoName,
+ 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
index c63c8ce..3893576 100644
--- a/polygerrit-ui/app/models/views/edit.ts
+++ b/polygerrit-ui/app/models/views/edit.ts
@@ -22,7 +22,7 @@
export interface EditViewState extends ViewState {
view: GerritView.EDIT;
changeNum: NumericChangeId;
- project: RepoName;
+ repo: RepoName;
path: string;
patchNum: RevisionPatchSetNum;
lineNum?: number;
@@ -43,8 +43,8 @@
suffix += state.lineNum;
}
- if (state.project) {
- const encodedProject = encodeURL(state.project, true);
+ if (state.repo) {
+ const encodedProject = encodeURL(state.repo, true);
return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
} else {
return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
diff --git a/polygerrit-ui/app/models/views/edit_test.ts b/polygerrit-ui/app/models/views/edit_test.ts
index 2912063..00bc805 100644
--- a/polygerrit-ui/app/models/views/edit_test.ts
+++ b/polygerrit-ui/app/models/views/edit_test.ts
@@ -18,7 +18,7 @@
const params: EditViewState = {
view: GerritView.EDIT,
changeNum: 42 as NumericChangeId,
- project: 'test-project' as RepoName,
+ repo: 'test-project' as RepoName,
path: 'x+y/path.cpp' as RepoName,
patchNum: 12 as RevisionPatchSetNum,
lineNum: 31,
diff --git a/polygerrit-ui/app/models/views/search.ts b/polygerrit-ui/app/models/views/search.ts
index 13de8f3..c5d394d 100644
--- a/polygerrit-ui/app/models/views/search.ts
+++ b/polygerrit-ui/app/models/views/search.ts
@@ -3,24 +3,75 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {RepoName, BranchName, TopicName} from '../../api/rest-api';
+import {combineLatest, fromEvent, Observable} from 'rxjs';
+import {
+ filter,
+ map,
+ startWith,
+ switchMap,
+ tap,
+ withLatestFrom,
+} from 'rxjs/operators';
+import {RepoName, BranchName, TopicName, ChangeInfo} from '../../api/rest-api';
+import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
+import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {GerritView} from '../../services/router/router-model';
-import {addQuotesWhen} from '../../utils/string-util';
+import {select} from '../../utils/observable-util';
+import {escapeAndWrapSearchOperatorValue} from '../../utils/string-util';
import {encodeURL, getBaseUrl} from '../../utils/url-util';
-import {define} from '../dependency';
+import {define, Provider} from '../dependency';
import {Model} from '../model';
+import {UserModel} from '../user/user-model';
import {ViewState} from './base';
+import {createChangeUrl} from './change';
+
+const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
+
+const REPO_QUERY_PATTERN =
+ /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+
+const LOOKUP_QUERY_PATTERNS: RegExp[] = [
+ /^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
+ /^\s*[1-9][0-9]*\s*$/g, // CHANGE_NUM
+ /[0-9a-f]{40}/, // COMMIT
+];
export interface SearchViewState extends ViewState {
view: GerritView.SEARCH;
- query?: string;
+
+ /**
+ * The query for searching changes.
+ *
+ * Changing this to something non-empty will trigger search.
+ */
+ query: string;
+
+ /**
+ * How many initial search results should be skipped? This is for showing
+ * more than one search result page. This must be a non-negative number.
+ * If the string is not provided or cannot be parsed as expected, then the
+ * offset falls back to 0.
+ *
+ * TODO: Consider converting from string to number before writing to the
+ * state object.
+ */
offset?: string;
+
+ /**
+ * Is a search API call currrently in progress?
+ */
+ loading: boolean;
+
+ /**
+ * The search results for the current query.
+ */
+ changes: ChangeInfo[];
}
export interface SearchUrlOptions {
query?: string;
offset?: number;
- project?: RepoName;
+ repo?: RepoName;
branch?: BranchName;
topic?: TopicName;
statuses?: string[];
@@ -42,8 +93,8 @@
if (params.owner) {
operators.push('owner:' + encodeURL(params.owner, false));
}
- if (params.project) {
- operators.push('project:' + encodeURL(params.project, false));
+ if (params.repo) {
+ operators.push('project:' + encodeURL(params.repo, false));
}
if (params.branch) {
operators.push('branch:' + encodeURL(params.branch, false));
@@ -51,18 +102,14 @@
if (params.topic) {
operators.push(
'topic:' +
- addQuotesWhen(
- encodeURL(params.topic, false),
- /[\s:]/.test(params.topic)
- )
+ escapeAndWrapSearchOperatorValue(encodeURL(params.topic, false))
);
}
if (params.hashtag) {
operators.push(
'hashtag:' +
- addQuotesWhen(
- encodeURL(params.hashtag.toLowerCase(), false),
- /[\s:]/.test(params.hashtag)
+ escapeAndWrapSearchOperatorValue(
+ encodeURL(params.hashtag.toLowerCase(), false)
)
);
}
@@ -86,8 +133,127 @@
export const searchViewModelToken =
define<SearchViewModel>('search-view-model');
+/**
+ * This is the view model for the search page.
+ *
+ * It keeps track of the overall search view state and provides selectors for
+ * subscribing to certain slices of the state.
+ *
+ * It manages loading the changes to be shown on the search page by providing
+ * `changes` in its state. Changes to the view state or certain user preferences
+ * will automatically trigger reloading the changes.
+ */
export class SearchViewModel extends Model<SearchViewState | undefined> {
- constructor() {
+ public readonly query$ = select(this.state$, s => s?.query ?? '');
+
+ private readonly offset$ = select(this.state$, s => s?.offset ?? '0');
+
+ /**
+ * Convenience selector for getting the `offset` as a number.
+ *
+ * TODO: Consider changing the type of `offset$` and `state.offset` to
+ * `number`.
+ */
+ public readonly offsetNumber$ = select(this.offset$, offset => {
+ const offsetNumber = Number(offset);
+ return Number.isFinite(offsetNumber) ? offsetNumber : 0;
+ });
+
+ public readonly changes$ = select(this.state$, s => s?.changes ?? []);
+
+ public readonly userId$ = select(
+ combineLatest([this.query$, this.changes$]),
+ ([query, changes]) => {
+ if (changes.length === 0) return undefined;
+ if (!USER_QUERY_PATTERN.test(query)) return undefined;
+ const owner = changes[0].owner;
+ return owner?._account_id ?? owner?.email;
+ }
+ );
+
+ public readonly repo$ = select(
+ combineLatest([this.query$, this.changes$]),
+ ([query, changes]) => {
+ if (changes.length === 0) return undefined;
+ if (!REPO_QUERY_PATTERN.test(query)) return undefined;
+ return changes[0].project;
+ }
+ );
+
+ public readonly loading$ = select(this.state$, s => s?.loading ?? false);
+
+ // For usage in `combineLatest` we need `startWith` such that reload$ has an
+ // initial value.
+ private readonly reload$: Observable<unknown> = fromEvent(
+ document,
+ 'reload'
+ ).pipe(startWith(undefined));
+
+ private readonly reloadChangesTrigger$ = combineLatest([
+ this.reload$,
+ this.query$,
+ this.offsetNumber$,
+ this.userModel.preferenceChangesPerPage$,
+ ]).pipe(
+ map(([_reload, query, offsetNumber, changesPerPage]) => {
+ const params: [string, number, number] = [
+ query,
+ offsetNumber,
+ changesPerPage,
+ ];
+ return params;
+ })
+ );
+
+ constructor(
+ private readonly restApiService: RestApiService,
+ private readonly userModel: UserModel,
+ private readonly getNavigation: Provider<NavigationService>
+ ) {
super(undefined);
+ this.subscriptions = [
+ this.reloadChangesTrigger$
+ .pipe(
+ switchMap(a => this.reloadChanges(a)),
+ tap(changes => this.updateState({changes, loading: false}))
+ )
+ .subscribe(),
+ this.changes$
+ .pipe(
+ filter(changes => changes.length === 1),
+ withLatestFrom(this.query$)
+ )
+ .subscribe(([changes, query]) =>
+ this.redirectSingleResult(query, changes)
+ ),
+ ];
+ }
+
+ private async reloadChanges([query, offset, changesPerPage]: [
+ string,
+ number,
+ number
+ ]): Promise<ChangeInfo[]> {
+ if (this.getState() === undefined) return [];
+ if (query.trim().length === 0) return [];
+ this.updateState({loading: true});
+ const changes = await this.restApiService.getChanges(
+ changesPerPage,
+ query,
+ offset
+ );
+ return changes ?? [];
+ }
+
+ // visible for testing
+ redirectSingleResult(query: string, changes: ChangeInfo[]): void {
+ if (changes.length !== 1) return;
+ for (const queryPattern of LOOKUP_QUERY_PATTERNS) {
+ if (query.match(queryPattern)) {
+ // "Back"/"Forward" buttons work correctly only with replaceUrl()
+ this.getNavigation().replaceUrl(createChangeUrl({change: changes[0]}));
+ return;
+ }
+ }
}
}
diff --git a/polygerrit-ui/app/models/views/search_test.ts b/polygerrit-ui/app/models/views/search_test.ts
index d48667b..6809225 100644
--- a/polygerrit-ui/app/models/views/search_test.ts
+++ b/polygerrit-ui/app/models/views/search_test.ts
@@ -4,15 +4,34 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '@open-wc/testing';
-import {BranchName, RepoName, TopicName} from '../../api/rest-api';
+import {SinonStub} from 'sinon';
+import {
+ AccountId,
+ BranchName,
+ EmailAddress,
+ NumericChangeId,
+ RepoName,
+ TopicName,
+} from '../../api/rest-api';
+import {navigationToken} from '../../elements/core/gr-navigation/gr-navigation';
import '../../test/common-test-setup';
-import {createSearchUrl, SearchUrlOptions} from './search';
+import {testResolver} from '../../test/common-test-setup';
+import {createChange} from '../../test/test-data-generators';
+import {
+ createSearchUrl,
+ SearchUrlOptions,
+ SearchViewModel,
+ searchViewModelToken,
+} from './search';
+
+const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
+const COMMIT_HASH = '12345678';
suite('search view state tests', () => {
test('createSearchUrl', () => {
let options: SearchUrlOptions = {
owner: 'a%b',
- project: 'c%d' as RepoName,
+ repo: 'c%d' as RepoName,
branch: 'e%f' as BranchName,
topic: 'g%h' as TopicName,
statuses: ['op%en'],
@@ -20,7 +39,7 @@
assert.equal(
createSearchUrl(options),
'/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
- 'topic:g%2525h+status:op%2525en'
+ 'topic:"g%2525h"+status:op%2525en'
);
window.CANONICAL_PATH = '/base';
@@ -31,7 +50,7 @@
assert.equal(
createSearchUrl(options),
'/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
- 'topic:g%2525h+status:op%2525en,100'
+ 'topic:"g%2525h"+status:op%2525en,100'
);
delete options.offset;
@@ -49,7 +68,7 @@
);
options = {topic: 'test' as TopicName};
- assert.equal(createSearchUrl(options), '/q/topic:test');
+ assert.equal(createSearchUrl(options), '/q/topic:"test"');
options = {topic: 'test test' as TopicName};
assert.equal(createSearchUrl(options), '/q/topic:"test+test"');
@@ -57,4 +76,130 @@
options = {topic: 'test:test' as TopicName};
assert.equal(createSearchUrl(options), '/q/topic:"test:test"');
});
+
+ suite('query based navigation', () => {
+ let replaceUrlStub: SinonStub;
+ let model: SearchViewModel;
+
+ setup(() => {
+ model = testResolver(searchViewModelToken);
+ replaceUrlStub = sinon.stub(testResolver(navigationToken), 'replaceUrl');
+ });
+
+ teardown(() => {
+ model.finalize();
+ });
+
+ test('Searching for a change ID redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+
+ model.redirectSingleResult(CHANGE_ID, [change]);
+
+ assert.isTrue(replaceUrlStub.called);
+ assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
+ });
+
+ test('Searching for a change num redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+
+ model.redirectSingleResult('1', [change]);
+
+ assert.isTrue(replaceUrlStub.called);
+ assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
+ });
+
+ test('Commit hash redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+
+ model.redirectSingleResult(COMMIT_HASH, [change]);
+
+ assert.isTrue(replaceUrlStub.called);
+ assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
+ });
+
+ test('No results: no redirect', async () => {
+ model.redirectSingleResult(CHANGE_ID, []);
+
+ assert.isFalse(replaceUrlStub.called);
+ });
+
+ test('More than 1 result: no redirect', async () => {
+ const change1 = {...createChange(), _number: 1 as NumericChangeId};
+ const change2 = {...createChange(), _number: 2 as NumericChangeId};
+
+ model.redirectSingleResult(CHANGE_ID, [change1, change2]);
+
+ assert.isFalse(replaceUrlStub.called);
+ });
+ });
+
+ suite('selectors', () => {
+ let model: SearchViewModel;
+ let userId: AccountId | EmailAddress | undefined;
+ let repo: RepoName | undefined;
+
+ setup(() => {
+ model = testResolver(searchViewModelToken);
+ model.userId$.subscribe(x => (userId = x));
+ model.repo$.subscribe(x => (repo = x));
+ });
+
+ teardown(() => {
+ model.finalize();
+ });
+
+ test('userId', async () => {
+ assert.isUndefined(userId);
+
+ model.updateState({
+ query: 'owner: foo@bar',
+ changes: [
+ {...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
+ ],
+ });
+ assert.equal(userId, 'foo@bar' as EmailAddress);
+
+ model.updateState({
+ query: 'foo bar baz',
+ changes: [
+ {...createChange(), owner: {email: 'foo@bar' as EmailAddress}},
+ ],
+ });
+ assert.isUndefined(userId);
+
+ model.updateState({
+ query: 'owner: foo@bar',
+ changes: [{...createChange(), owner: {}}],
+ });
+ assert.isUndefined(userId);
+ });
+
+ test('repo', async () => {
+ assert.isUndefined(repo);
+
+ model.updateState({
+ query: 'foo bar baz',
+ changes: [{...createChange(), project: 'test-repo' as RepoName}],
+ });
+ assert.isUndefined(repo);
+
+ model.updateState({
+ query: 'foo bar baz',
+ changes: [{...createChange()}],
+ });
+ assert.isUndefined(repo);
+
+ model.updateState({
+ query: 'project: test-repo',
+ changes: [{...createChange(), project: 'test-repo' as RepoName}],
+ });
+ assert.equal(repo, 'test-repo' as RepoName);
+
+ model.updateState({
+ query: 'project:test-repo status:open',
+ changes: [{...createChange(), project: 'test-repo' as RepoName}],
+ });
+ assert.equal(repo, 'test-repo' as RepoName);
+ });
+ });
});
diff --git a/polygerrit-ui/app/scripts/hiddenscroll.ts b/polygerrit-ui/app/scripts/hiddenscroll.ts
deleted file mode 100644
index e95a362..0000000
--- a/polygerrit-ui/app/scripts/hiddenscroll.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-let hiddenscroll: boolean | undefined = undefined;
-
-window.addEventListener('WebComponentsReady', () => {
- const elem = document.createElement('div');
- elem.setAttribute('style', 'width:100px;height:100px;overflow:scroll');
- document.body.appendChild(elem);
- hiddenscroll = elem.offsetWidth === elem.clientWidth;
- elem.remove();
-});
-
-export function _setHiddenScroll(value: boolean) {
- hiddenscroll = value;
-}
-
-export function getHiddenScroll() {
- return hiddenscroll;
-}
diff --git a/polygerrit-ui/app/scripts/rootElement.ts b/polygerrit-ui/app/scripts/rootElement.ts
deleted file mode 100644
index 1dbb2a1..0000000
--- a/polygerrit-ui/app/scripts/rootElement.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/**
- * Returns the root element of the dom: body.
- */
-export const getRootElement = () => document.body;
diff --git a/polygerrit-ui/app/scripts/util.ts b/polygerrit-ui/app/scripts/util.ts
deleted file mode 100644
index b785a71..0000000
--- a/polygerrit-ui/app/scripts/util.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * @license
- * Copyright 2015 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export interface CancelablePromise<T> extends Promise<T> {
- cancel(): void;
-}
-
-/**
- * Make the promise cancelable.
- *
- * Returns a promise with a `cancel()` method wrapped around `promise`.
- * Calling `cancel()` will reject the returned promise with
- * {isCancelled: true} synchronously. If the inner promise for a cancelled
- * promise resolves or rejects this is ignored.
- */
-export function makeCancelable<T>(promise: Promise<T>) {
- // True if the promise is either resolved or reject (possibly cancelled)
- let isDone = false;
-
- let rejectPromise: (reason?: unknown) => void;
-
- const wrappedPromise: CancelablePromise<T> = new Promise(
- (resolve, reject) => {
- rejectPromise = reject;
- promise.then(
- val => {
- if (!isDone) resolve(val);
- isDone = true;
- },
- error => {
- if (!isDone) reject(error);
- isDone = true;
- }
- );
- }
- ) as CancelablePromise<T>;
-
- wrappedPromise.cancel = () => {
- if (isDone) return;
- rejectPromise({isCanceled: true});
- isDone = true;
- };
- return wrappedPromise;
-}
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 2e1b817..040ac0c 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -8,20 +8,18 @@
import {DependencyToken} from '../models/dependency';
import {FlagsServiceImplementation} from './flags/flags_impl';
import {GrReporting} from './gr-reporting/gr-reporting_impl';
-import {EventEmitter} from './gr-event-interface/gr-event-interface_impl';
import {Auth} from './gr-auth/gr-auth_impl';
import {GrRestApiServiceImpl} from './gr-rest-api/gr-rest-api-impl';
import {ChangeModel, changeModelToken} from '../models/change/change-model';
import {FilesModel, filesModelToken} from '../models/change/files-model';
import {ChecksModel, checksModelToken} from '../models/checks/checks-model';
-import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
-import {GrStorageService} from './storage/gr-storage_impl';
-import {UserModel} from '../models/user/user-model';
+import {GrStorageService, storageServiceToken} from './storage/gr-storage_impl';
+import {UserModel, userModelToken} from '../models/user/user-model';
import {
CommentsModel,
commentsModelToken,
} from '../models/comments/comments-model';
-import {RouterModel} from './router/router-model';
+import {RouterModel, routerModelToken} from './router/router-model';
import {
ShortcutsService,
shortcutsServiceToken,
@@ -29,9 +27,14 @@
import {assertIsDefined} from '../utils/common-util';
import {ConfigModel, configModelToken} from '../models/config/config-model';
import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
-import {PluginsModel} from '../models/plugins/plugins-model';
-import {HighlightService} from './highlight/highlight-service';
-import {AccountsModel} from '../models/accounts-model/accounts-model';
+import {
+ HighlightService,
+ highlightServiceToken,
+} from './highlight/highlight-service';
+import {
+ AccountsModel,
+ accountsModelToken,
+} from '../models/accounts-model/accounts-model';
import {
DashboardViewModel,
dashboardViewModelToken,
@@ -58,147 +61,155 @@
import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
import {SearchViewModel, searchViewModelToken} from '../models/views/search';
import {navigationToken} from '../elements/core/gr-navigation/gr-navigation';
+import {
+ PluginLoader,
+ pluginLoaderToken,
+} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
+import {authServiceToken} from './gr-auth/gr-auth';
/**
* The AppContext lazy initializator for all services
*/
export function createAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
- routerModel: (_ctx: Partial<AppContext>) => new RouterModel(),
flagsService: (_ctx: Partial<AppContext>) =>
new FlagsServiceImplementation(),
reportingService: (ctx: Partial<AppContext>) => {
assertIsDefined(ctx.flagsService, 'flagsService)');
return new GrReporting(ctx.flagsService);
},
- eventEmitter: (_ctx: Partial<AppContext>) => new EventEmitter(),
- authService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.eventEmitter, 'eventEmitter');
- return new Auth(ctx.eventEmitter);
- },
+ authService: (_ctx: Partial<AppContext>) => new Auth(),
restApiService: (ctx: Partial<AppContext>) => {
assertIsDefined(ctx.authService, 'authService');
- assertIsDefined(ctx.flagsService, 'flagsService');
- return new GrRestApiServiceImpl(ctx.authService, ctx.flagsService);
- },
- jsApiService: (ctx: Partial<AppContext>) => {
- const reportingService = ctx.reportingService;
- assertIsDefined(reportingService, 'reportingService');
- return new GrJsApiInterface(reportingService);
- },
- storageService: (_ctx: Partial<AppContext>) => new GrStorageService(),
- userModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new UserModel(ctx.restApiService);
- },
- accountsModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new AccountsModel(ctx.restApiService);
- },
- pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
- highlightService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new HighlightService(ctx.reportingService);
+ return new GrRestApiServiceImpl(ctx.authService);
},
};
return create<AppContext>(appRegistry);
}
+export type Creator<T> = () => T & Finalizable;
+
+// Dependencies are provided as creator functions to ensure that they are
+// not created until they are utilized.
+// This is mainly useful in tests: E.g. don't create a
+// change-model in change-model_test.ts because it creates one in the test
+// after setting up stubs.
export function createAppDependencies(
- appContext: AppContext
-): Map<DependencyToken<unknown>, Finalizable> {
- const dependencies = new Map<DependencyToken<unknown>, Finalizable>();
- const browserModel = new BrowserModel(appContext.userModel);
- dependencies.set(browserModelToken, browserModel);
-
- const adminViewModel = new AdminViewModel();
- dependencies.set(adminViewModelToken, adminViewModel);
- const agreementViewModel = new AgreementViewModel();
- dependencies.set(agreementViewModelToken, agreementViewModel);
- const changeViewModel = new ChangeViewModel();
- dependencies.set(changeViewModelToken, changeViewModel);
- const dashboardViewModel = new DashboardViewModel();
- dependencies.set(dashboardViewModelToken, dashboardViewModel);
- const diffViewModel = new DiffViewModel();
- dependencies.set(diffViewModelToken, diffViewModel);
- const documentationViewModel = new DocumentationViewModel();
- dependencies.set(documentationViewModelToken, documentationViewModel);
- const editViewModel = new EditViewModel();
- dependencies.set(editViewModelToken, editViewModel);
- const groupViewModel = new GroupViewModel();
- dependencies.set(groupViewModelToken, groupViewModel);
- const pluginViewModel = new PluginViewModel();
- dependencies.set(pluginViewModelToken, pluginViewModel);
- const repoViewModel = new RepoViewModel();
- dependencies.set(repoViewModelToken, repoViewModel);
- const searchViewModel = new SearchViewModel();
- dependencies.set(searchViewModelToken, searchViewModel);
- const settingsViewModel = new SettingsViewModel();
- dependencies.set(settingsViewModelToken, settingsViewModel);
-
- const router = new GrRouter(
- appContext.reportingService,
- appContext.routerModel,
- appContext.restApiService,
- adminViewModel,
- agreementViewModel,
- changeViewModel,
- dashboardViewModel,
- diffViewModel,
- documentationViewModel,
- editViewModel,
- groupViewModel,
- pluginViewModel,
- repoViewModel,
- searchViewModel,
- settingsViewModel
- );
- dependencies.set(routerToken, router);
- dependencies.set(navigationToken, router);
-
- const changeModel = new ChangeModel(
- appContext.routerModel,
- appContext.restApiService,
- appContext.userModel
- );
- dependencies.set(changeModelToken, changeModel);
-
- const accountsModel = new AccountsModel(appContext.restApiService);
-
- const commentsModel = new CommentsModel(
- appContext.routerModel,
- changeModel,
- accountsModel,
- appContext.restApiService,
- appContext.reportingService
- );
- dependencies.set(commentsModelToken, commentsModel);
-
- const filesModel = new FilesModel(
- changeModel,
- commentsModel,
- appContext.restApiService
- );
- dependencies.set(filesModelToken, filesModel);
-
- const configModel = new ConfigModel(changeModel, appContext.restApiService);
- dependencies.set(configModelToken, configModel);
-
- const checksModel = new ChecksModel(
- appContext.routerModel,
- changeViewModel,
- changeModel,
- appContext.reportingService,
- appContext.pluginsModel
- );
-
- dependencies.set(checksModelToken, checksModel);
-
- const shortcutsService = new ShortcutsService(
- appContext.userModel,
- appContext.reportingService
- );
- dependencies.set(shortcutsServiceToken, shortcutsService);
-
- return dependencies;
+ appContext: AppContext,
+ resolver: <T>(token: DependencyToken<T>) => T
+): Map<DependencyToken<unknown>, Creator<unknown>> {
+ return new Map<DependencyToken<unknown>, Creator<unknown>>([
+ [authServiceToken, () => appContext.authService],
+ [routerModelToken, () => new RouterModel()],
+ [userModelToken, () => new UserModel(appContext.restApiService)],
+ [browserModelToken, () => new BrowserModel(resolver(userModelToken))],
+ [accountsModelToken, () => new AccountsModel(appContext.restApiService)],
+ [adminViewModelToken, () => new AdminViewModel()],
+ [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()],
+ [
+ searchViewModelToken,
+ () =>
+ new SearchViewModel(
+ appContext.restApiService,
+ resolver(userModelToken),
+ () => resolver(navigationToken)
+ ),
+ ],
+ [settingsViewModelToken, () => new SettingsViewModel()],
+ [
+ routerToken,
+ () =>
+ new GrRouter(
+ appContext.reportingService,
+ resolver(routerModelToken),
+ appContext.restApiService,
+ resolver(adminViewModelToken),
+ resolver(agreementViewModelToken),
+ resolver(changeViewModelToken),
+ resolver(dashboardViewModelToken),
+ resolver(diffViewModelToken),
+ resolver(documentationViewModelToken),
+ resolver(editViewModelToken),
+ resolver(groupViewModelToken),
+ resolver(pluginViewModelToken),
+ resolver(repoViewModelToken),
+ resolver(searchViewModelToken),
+ resolver(settingsViewModelToken)
+ ),
+ ],
+ [navigationToken, () => resolver(routerToken)],
+ [
+ changeModelToken,
+ () =>
+ new ChangeModel(
+ resolver(routerModelToken),
+ appContext.restApiService,
+ resolver(userModelToken)
+ ),
+ ],
+ [
+ commentsModelToken,
+ () =>
+ new CommentsModel(
+ resolver(routerModelToken),
+ resolver(changeModelToken),
+ resolver(accountsModelToken),
+ appContext.restApiService,
+ appContext.reportingService
+ ),
+ ],
+ [
+ filesModelToken,
+ () =>
+ new FilesModel(
+ resolver(changeModelToken),
+ resolver(commentsModelToken),
+ appContext.restApiService
+ ),
+ ],
+ [
+ configModelToken,
+ () =>
+ new ConfigModel(resolver(changeModelToken), appContext.restApiService),
+ ],
+ [
+ pluginLoaderToken,
+ () =>
+ new PluginLoader(
+ appContext.reportingService,
+ appContext.restApiService
+ ),
+ ],
+ [
+ checksModelToken,
+ () =>
+ new ChecksModel(
+ resolver(changeViewModelToken),
+ resolver(changeModelToken),
+ appContext.reportingService,
+ resolver(pluginLoaderToken).pluginsModel
+ ),
+ ],
+ [
+ shortcutsServiceToken,
+ () =>
+ new ShortcutsService(
+ resolver(userModelToken),
+ appContext.reportingService
+ ),
+ ],
+ [storageServiceToken, () => new GrStorageService()],
+ [
+ highlightServiceToken,
+ () => new HighlightService(appContext.reportingService),
+ ],
+ ]);
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 5f47c43..aa2c032 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -5,31 +5,15 @@
*/
import {Finalizable} from './registry';
import {FlagsService} from './flags/flags';
-import {EventEmitterService} from './gr-event-interface/gr-event-interface';
import {ReportingService} from './gr-reporting/gr-reporting';
import {AuthService} from './gr-auth/gr-auth';
import {RestApiService} from './gr-rest-api/gr-rest-api';
-import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
-import {StorageService} from './storage/gr-storage';
-import {UserModel} from '../models/user/user-model';
-import {RouterModel} from './router/router-model';
-import {PluginsModel} from '../models/plugins/plugins-model';
-import {HighlightService} from './highlight/highlight-service';
-import {AccountsModel} from '../models/accounts-model/accounts-model';
export interface AppContext {
- routerModel: RouterModel;
flagsService: FlagsService;
reportingService: ReportingService;
- eventEmitter: EventEmitterService;
authService: AuthService;
restApiService: RestApiService;
- jsApiService: JsApiService;
- storageService: StorageService;
- userModel: UserModel;
- accountsModel: AccountsModel;
- pluginsModel: PluginsModel;
- highlightService: HighlightService;
}
/**
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 0bf1522..efc0f00 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -20,7 +20,6 @@
DIFF_RENDERING_LIT = 'UiFeature__diff_rendering_lit',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
SUGGEST_EDIT = 'UiFeature__suggest_edit',
- CHECKS_FIXES = 'UiFeature__checks_fixes',
MENTION_USERS = 'UiFeature__mention_users',
RENDER_MARKDOWN = 'UiFeature__render_markdown',
AUTO_APP_THEME = 'UiFeature__auto_app_theme',
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth.ts b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
index 1dc4a84..168fb26 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
@@ -3,6 +3,7 @@
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {define} from '../../models/dependency';
import {Finalizable} from '../registry';
export enum AuthType {
XSRF_TOKEN = 'xsrf_token',
@@ -33,6 +34,7 @@
// Auth class supports only Headers in options
headers?: Headers;
}
+export const authServiceToken = define<AuthService>('auth-service');
export interface AuthService extends Finalizable {
baseUrl: string;
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index 8a4e51f..4195666 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -3,8 +3,8 @@
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {fire} from '../../utils/event-util';
import {getBaseUrl} from '../../utils/url-util';
-import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
import {Finalizable} from '../registry';
import {
AuthRequestInit,
@@ -67,11 +67,8 @@
private getToken: GetTokenCallback;
- public eventEmitter: EventEmitterService;
-
- constructor(eventEmitter: EventEmitterService) {
+ constructor() {
this.getToken = () => Promise.resolve(this.cachedTokenPromise);
- this.eventEmitter = eventEmitter;
}
get baseUrl() {
@@ -130,7 +127,7 @@
if (this._status === status) return;
if (this._status === AuthStatus.AUTHED) {
- this.eventEmitter.emit('auth-error', {
+ fire(document, 'auth-error', {
message: Auth.CREDS_EXPIRED_MSG,
action: 'Refresh credentials',
});
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
index cc34681e..480484e 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
@@ -3,7 +3,6 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
import {
AuthRequestInit,
AuthService,
@@ -18,11 +17,7 @@
private _status = AuthStatus.UNDETERMINED;
- public eventEmitter: EventEmitterService;
-
- constructor(eventEmitter: EventEmitterService) {
- this.eventEmitter = eventEmitter;
- }
+ constructor() {}
get isAuthed() {
return this._status === Auth.STATUS.AUTHED;
@@ -33,10 +28,14 @@
private _setStatus(status: AuthStatus) {
if (this._status === status) return;
if (this._status === AuthStatus.AUTHED) {
- this.eventEmitter.emit('auth-error', {
- message: Auth.CREDS_EXPIRED_MSG,
- action: 'Refresh credentials',
- });
+ document.dispatchEvent(
+ new CustomEvent('auth-error', {
+ detail: {
+ message: Auth.CREDS_EXPIRED_MSG,
+ action: 'Refresh credentials',
+ },
+ })
+ );
}
this._status = status;
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
index 4552dad..5d6056d 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
@@ -5,22 +5,16 @@
*/
import '../../test/common-test-setup';
import {Auth} from './gr-auth_impl';
-import {getAppContext} from '../app-context';
import {stubBaseUrl} from '../../test/test-utils';
-import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
import {SinonFakeTimers} from 'sinon';
import {AuthRequestInit, DefaultAuthOptions} from './gr-auth';
import {assert} from '@open-wc/testing';
suite('gr-auth', () => {
let auth: Auth;
- let eventEmitter: EventEmitterService;
setup(() => {
- // TODO(poucet): Mock the eventEmitter completely instead of getting it
- // from appContext.
- eventEmitter = getAppContext().eventEmitter;
- auth = new Auth(eventEmitter);
+ auth = new Auth();
});
suite('Auth class methods', () => {
@@ -118,11 +112,13 @@
assert.equal(auth.status, Auth.STATUS.AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 403}));
- const emitStub = sinon.stub(eventEmitter, 'emit');
+ const emitStub = sinon.stub();
+ document.addEventListener('auth-error', emitStub);
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
assert.isTrue(emitStub.called);
+ document.removeEventListener('auth-error', emitStub);
});
test('fire event when switch from authed to error', async () => {
@@ -132,11 +128,13 @@
assert.equal(auth.status, Auth.STATUS.AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub(eventEmitter, 'emit');
+ const emitStub = sinon.stub();
+ document.addEventListener('auth-error', emitStub);
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.isTrue(emitStub.called);
assert.equal(auth.status, Auth.STATUS.ERROR);
+ document.removeEventListener('auth-error', emitStub);
});
test('no event from non-authed to other status', async () => {
@@ -146,11 +144,13 @@
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 204}));
- const emitStub = sinon.stub(eventEmitter, 'emit');
+ const emitStub = sinon.stub();
+ document.addEventListener('auth-error', emitStub);
const authed2 = await auth.authCheck();
assert.isTrue(authed2);
assert.isFalse(emitStub.called);
assert.equal(auth.status, Auth.STATUS.AUTHED);
+ document.removeEventListener('auth-error', emitStub);
});
test('no event from non-authed to other status', async () => {
@@ -160,11 +160,13 @@
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub(eventEmitter, 'emit');
+ const emitStub = sinon.stub();
+ document.addEventListener('auth-error', emitStub);
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.isFalse(emitStub.called);
assert.equal(auth.status, Auth.STATUS.ERROR);
+ document.removeEventListener('auth-error', emitStub);
});
});
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
deleted file mode 100644
index 4153b3d..0000000
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {Finalizable} from '../registry';
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type EventCallback = (...args: any) => void;
-export type UnsubscribeMethod = () => void;
-
-export interface EventEmitterService extends Finalizable {
- /**
- * Register an event listener to an event.
- */
- addListener(eventName: string, cb: EventCallback): UnsubscribeMethod;
-
- /**
- * Alias for addListener.
- */
- on(eventName: string, cb: EventCallback): UnsubscribeMethod;
-
- /**
- * Attach event handler only once. Automatically removed.
- */
- once(eventName: string, cb: EventCallback): UnsubscribeMethod;
-
- /**
- * De-register an event listener to an event.
- */
- removeListener(eventName: string, cb: EventCallback): void;
-
- /**
- * Alias to removeListener
- */
- off(eventName: string, cb: EventCallback): void;
-
- /**
- * Synchronously calls each of the listeners registered for
- * the event named eventName, in the order they were registered,
- * passing the supplied detail to each.
- *
- * @return true if the event had listeners, false otherwise.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- emit(eventName: string, detail: any): boolean;
-
- /**
- * Alias to emit.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- dispatch(eventName: string, detail: any): boolean;
-
- /**
- * Remove listeners for a specific event or all.
- *
- * @param eventName if not provided, will remove all
- */
- removeAllListeners(eventName: string): void;
-}
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
deleted file mode 100644
index 7228282..0000000
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @license
- * Copyright 2019 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {Finalizable} from '../registry';
-import {
- EventCallback,
- EventEmitterService,
- UnsubscribeMethod,
-} from './gr-event-interface';
-/**
- * An lite implementation of
- * https://nodejs.org/api/events.html#events_class_eventemitter.
- *
- * This is unrelated to the native DOM events, you should use it when you want
- * to enable EventEmitter interface on any class.
- *
- * @example
- *
- * class YourClass extends EventEmitter {
- * // now all instance of YourClass will have this EventEmitter interface
- * }
- *
- */
-export class EventEmitter implements EventEmitterService, Finalizable {
- private _listenersMap = new Map<string, EventCallback[]>();
-
- finalize() {
- this.removeAllListeners();
- }
-
- /**
- * Register an event listener to an event.
- */
- addListener(eventName: string, cb: EventCallback): UnsubscribeMethod {
- if (!eventName || !cb) {
- console.warn('A valid eventname and callback is required!');
- return () => {};
- }
-
- const listeners = this._listenersMap.get(eventName) || [];
- listeners.push(cb);
- this._listenersMap.set(eventName, listeners);
-
- return () => {
- this.off(eventName, cb);
- };
- }
-
- /**
- * Alias for addListener.
- */
- on(eventName: string, cb: EventCallback): UnsubscribeMethod {
- return this.addListener(eventName, cb);
- }
-
- /**
- * Attach event handler only once. Automatically removed.
- */
- once(eventName: string, cb: EventCallback): UnsubscribeMethod {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const onceWrapper = (...args: any[]) => {
- cb(...args);
- this.off(eventName, onceWrapper);
- };
- return this.on(eventName, onceWrapper);
- }
-
- /**
- * De-register an event listener to an event.
- */
- removeListener(eventName: string, cb: EventCallback): void {
- let listeners = this._listenersMap.get(eventName) || [];
- listeners = listeners.filter(listener => listener !== cb);
- this._listenersMap.set(eventName, listeners);
- }
-
- /**
- * Alias to removeListener
- */
- off(eventName: string, cb: EventCallback): void {
- this.removeListener(eventName, cb);
- }
-
- /**
- * Synchronously calls each of the listeners registered for
- * the event named eventName, in the order they were registered,
- * passing the supplied detail to each.
- *
- * @return true if the event had listeners, false otherwise.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- emit(eventName: string, detail: any): boolean {
- const listeners = this._listenersMap.get(eventName) || [];
- for (const listener of listeners) {
- try {
- listener(detail);
- } catch (e) {
- console.error(e);
- }
- }
- return listeners.length !== 0;
- }
-
- /**
- * Alias to emit.
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- dispatch(eventName: string, detail: any): boolean {
- return this.emit(eventName, detail);
- }
-
- /**
- * Remove listeners for a specific event or all.
- *
- * @param eventName if not provided, will remove all
- */
- removeAllListeners(eventName?: string): void {
- if (eventName) {
- this._listenersMap.set(eventName, []);
- } else {
- this._listenersMap = new Map<string, EventCallback[]>();
- }
- }
-}
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
deleted file mode 100644
index a63eda3..0000000
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * @license
- * Copyright 2019 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../test/common-test-setup';
-import {mockPromise} from '../../test/test-utils';
-import {EventEmitter} from './gr-event-interface_impl';
-import {assert} from '@open-wc/testing';
-
-suite('gr-event-interface tests', () => {
- let gerrit;
- setup(() => {
- gerrit = window.Gerrit;
- });
-
- suite('test on Gerrit', () => {
- setup(() => {
- gerrit.removeAllListeners();
- });
-
- test('communicate between plugin and Gerrit', async () => {
- const eventName = 'test-plugin-event';
- let p;
- const promise = mockPromise();
- gerrit.on(eventName, e => {
- assert.equal(e.value, 'test');
- assert.equal(e.plugin, p);
- promise.resolve();
- });
- gerrit.install(plugin => {
- p = plugin;
- gerrit.emit(eventName, {value: 'test', plugin});
- }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- await promise;
- });
-
- test('listen on events from core', async () => {
- const eventName = 'test-plugin-event';
- const promise = mockPromise();
- gerrit.on(eventName, e => {
- assert.equal(e.value, 'test');
- promise.resolve();
- });
-
- gerrit.emit(eventName, {value: 'test'});
- await promise;
- });
-
- test('communicate across plugins', async () => {
- const eventName = 'test-plugin-event';
- const promise = mockPromise();
- gerrit.install(plugin => {
- gerrit.on(eventName, e => {
- assert.equal(e.plugin.getPluginName(), 'testB');
- promise.resolve();
- });
- }, '0.1',
- 'http://test.com/plugins/testA/static/testA.js');
-
- gerrit.install(plugin => {
- gerrit.emit(eventName, {plugin});
- }, '0.1',
- 'http://test.com/plugins/testB/static/testB.js');
- await promise;
- });
- });
-
- suite('test on interfaces', () => {
- let testObj;
-
- class TestClass extends EventEmitter {
- }
-
- setup(() => {
- testObj = new TestClass();
- });
-
- test('on', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.emit('test');
- testObj.emit('test');
- assert.isTrue(cbStub.calledTwice);
- });
-
- test('once', () => {
- const cbStub = sinon.stub();
- testObj.once('test', cbStub);
- testObj.emit('test');
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('unsubscribe', () => {
- const cbStub = sinon.stub();
- const unsubscribe = testObj.on('test', cbStub);
- testObj.emit('test');
- unsubscribe();
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('off', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.emit('test');
- testObj.off('test', cbStub);
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('removeAllListeners', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.removeAllListeners('test');
- testObj.emit('test');
- assert.isTrue(cbStub.notCalled);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 841d22e..9b7986b 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -20,7 +20,6 @@
import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {parseDate} from '../../utils/date-util';
import {getBaseUrl} from '../../utils/url-util';
-import {getAppContext} from '../app-context';
import {Finalizable} from '../registry';
import {getParentIndex, isMergeParent} from '../../utils/patch-set-util';
import {
@@ -100,7 +99,7 @@
PreferencesInfo,
PreferencesInput,
ProjectAccessInfo,
- ProjectAccessInfoMap,
+ RepoAccessInfoMap,
ProjectAccessInput,
ProjectInfo,
ProjectInfoWithName,
@@ -146,7 +145,7 @@
import {addDraftProp, DraftInfo} from '../../utils/comment-util';
import {BaseScheduler} from '../scheduler/scheduler';
import {MaxInFlightScheduler} from '../scheduler/max-in-flight-scheduler';
-import {FlagsService} from '../flags/flags';
+import {escapeAndWrapSearchOperatorValue} from '../../utils/string-util';
const MAX_PROJECT_RESULTS = 25;
@@ -255,13 +254,13 @@
type SendChangeRequest = SendRawChangeRequest | SendJSONChangeRequest;
-export function _testOnlyResetGrRestApiSharedObjects() {
+export function testOnlyResetGrRestApiSharedObjects(authService: AuthService) {
siteBasedCache = new SiteBasedCache();
fetchPromisesCache = new FetchPromisesCache();
pendingRequest = {};
grEtagDecorator = new GrEtagDecorator();
projectLookup = {};
- getAppContext().authService.clearCache();
+ authService.clearCache();
}
function createReadScheduler() {
@@ -280,16 +279,14 @@
readonly _etags = grEtagDecorator; // Shared across instances.
- readonly _projectLookup = projectLookup; // Shared across instances.
+ // readonly, but set in tests.
+ _projectLookup = projectLookup; // Shared across instances.
// The value is set in created, before any other actions
- private readonly _restApiHelper: GrRestApiHelper;
+ // Private, but used in tests.
+ readonly _restApiHelper: GrRestApiHelper;
- constructor(
- private readonly authService: AuthService,
- // @ts-ignore: it's ok.
- private readonly _flagsService: FlagsService
- ) {
+ constructor(private readonly authService: AuthService) {
this._restApiHelper = new GrRestApiHelper(
this._cache,
this.authService,
@@ -350,13 +347,13 @@
}) as Promise<ConfigInfo | undefined>;
}
- getRepoAccess(repo: RepoName): Promise<ProjectAccessInfoMap | undefined> {
+ getRepoAccess(repo: RepoName): Promise<RepoAccessInfoMap | undefined> {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._fetchSharedCacheURL({
url: '/access/?project=' + encodeURIComponent(repo),
anonymizedUrl: '/access/?project=*',
- }) as Promise<ProjectAccessInfoMap | undefined>;
+ }) as Promise<RepoAccessInfoMap | undefined>;
}
getRepoDashboards(
@@ -1631,7 +1628,7 @@
}) as Promise<GroupNameToGroupInfoMap | undefined>;
}
- getSuggestedProjects(
+ getSuggestedRepos(
inputVal: string,
n?: number
): Promise<NameToProjectInfoMap | undefined> {
@@ -1660,7 +1657,11 @@
const queryParams = [];
inputVal = inputVal?.trim() ?? '';
if (inputVal.length > 0) {
- queryParams.push(inputVal);
+ // Wrap in quotes so that reserved keywords do not throw an error such
+ // as typing "and"
+ // Espace quotes in user input since we are wrapping input in quotes
+ // explicitly
+ queryParams.push(`${escapeAndWrapSearchOperatorValue(inputVal)}`);
}
if (canSee) {
queryParams.push(`cansee:${canSee}`);
@@ -1773,7 +1774,7 @@
}
getChangeCherryPicks(
- project: RepoName,
+ repo: RepoName,
changeID: ChangeId,
changeNum: NumericChangeId
): Promise<ChangeInfo[] | undefined> {
@@ -1782,7 +1783,7 @@
ListChangesOption.CURRENT_COMMIT
);
const query = [
- `project:${project}`,
+ `project:${repo}`,
`change:${changeID}`,
`-change:${changeNum}`,
'-is:abandoned',
@@ -1811,7 +1812,7 @@
ListChangesOption.CURRENT_COMMIT,
ListChangesOption.DETAILED_LABELS
);
- const queryTerms = [`topic:"${topic}"`];
+ const queryTerms = [`topic:${escapeAndWrapSearchOperatorValue(topic)}`];
if (options?.openChangesOnly) {
queryTerms.push('status:open');
}
@@ -1830,7 +1831,7 @@
}
getChangesWithSimilarTopic(topic: string): Promise<ChangeInfo[] | undefined> {
- const query = `intopic:"${topic}"`;
+ const query = `intopic:${escapeAndWrapSearchOperatorValue(topic)}`;
return this._restApiHelper.fetchJSON({
url: '/changes/',
params: {q: query},
@@ -1841,7 +1842,7 @@
getChangesWithSimilarHashtag(
hashtag: string
): Promise<ChangeInfo[] | undefined> {
- const query = `inhashtag:"${hashtag}"`;
+ const query = `inhashtag:${escapeAndWrapSearchOperatorValue(hashtag)}`;
return this._restApiHelper.fetchJSON({
url: '/changes/',
params: {q: query},
@@ -1929,7 +1930,7 @@
}
createChange(
- project: RepoName,
+ repo: RepoName,
branch: BranchName,
subject: string,
topic?: string,
@@ -1942,7 +1943,7 @@
method: HttpMethod.POST,
url: '/changes/',
body: {
- project,
+ project: repo,
branch,
subject,
topic,
@@ -2638,13 +2639,13 @@
}
getCommitInfo(
- project: RepoName,
+ repo: RepoName,
commit: CommitId
): Promise<CommitInfo | undefined> {
return this._restApiHelper.fetchJSON({
url:
'/projects/' +
- encodeURIComponent(project) +
+ encodeURIComponent(repo) +
'/commits/' +
encodeURIComponent(commit),
anonymizedUrl: '/projects/*/comments/*',
@@ -2742,15 +2743,9 @@
_changeBaseURL(
changeNum: NumericChangeId,
- revisionId?: RevisionId,
- project?: RepoName
+ revisionId?: RevisionId
): Promise<string> {
- // TODO(kaspern): For full slicer migration, app should warn with a call
- // stack every time _changeBaseURL is called without a project.
- const projectPromise = project
- ? Promise.resolve(project)
- : this.getFromProjectLookup(changeNum);
- return projectPromise.then(project => {
+ return this.getFromProjectLookup(changeNum).then(project => {
// TODO(TS): unclear why project can't be null here. Fix it
let url = `/changes/${encodeURIComponent(
project as RepoName
@@ -3079,9 +3074,6 @@
_getChangeURLAndSend(req: SendJSONChangeRequest): Promise<ParsedJSON>;
- /**
- * Alias for _changeBaseURL.then(send).
- */
_getChangeURLAndSend(
req: SendChangeRequest
): Promise<ParsedJSON | Response | undefined> {
@@ -3109,9 +3101,6 @@
});
}
- /**
- * Alias for _changeBaseURL.then(_fetchJSON).
- */
_getChangeURLAndFetch(
req: FetchChangeJSON,
noAcceptHeader?: boolean
@@ -3224,13 +3213,13 @@
}
getDashboard(
- project: RepoName,
+ repo: RepoName,
dashboard: DashboardId,
errFn?: ErrorCallback
): Promise<DashboardInfo | undefined> {
const url =
'/projects/' +
- encodeURIComponent(project) +
+ encodeURIComponent(repo) +
'/dashboards/' +
encodeURIComponent(dashboard);
return this._fetchSharedCacheURL({
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
deleted file mode 100644
index 5af123a..0000000
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
+++ /dev/null
@@ -1,1552 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../test/common-test-setup';
-import {
- addListenerForTest,
- mockPromise,
- stubAuth,
- waitEventLoop,
-} from '../../test/test-utils';
-import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
-import {
- ListChangesOption,
- listChangesOptionsToHex,
-} from '../../utils/change-util';
-import {getAppContext} from '../app-context';
-import {createChange} from '../../test/test-data-generators';
-import {CURRENT} from '../../utils/patch-set-util';
-import {
- parsePrefixedJSON,
- readResponsePayload,
- JSON_PREFIX,
- // eslint-disable-next-line max-len
-} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
-import {GrRestApiServiceImpl} from './gr-rest-api-impl';
-import {CommentSide} from '../../constants/constants';
-import {EDIT, PARENT} from '../../types/common';
-import {assert} from '@open-wc/testing';
-
-const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
- ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.CURRENT_ACTIONS,
- ListChangesOption.CURRENT_REVISION,
- ListChangesOption.DETAILED_LABELS,
- ListChangesOption.SUBMIT_REQUIREMENTS
-);
-
-suite('gr-rest-api-service-impl tests', () => {
- let element;
-
- let ctr = 0;
- let originalCanonicalPath;
-
- setup(() => {
- // Modify CANONICAL_PATH to effectively reset cache.
- ctr += 1;
- originalCanonicalPath = window.CANONICAL_PATH;
- window.CANONICAL_PATH = `test${ctr}`;
-
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sinon.stub(window, 'fetch').returns(
- Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- })
- );
- // fake auth
- sinon
- .stub(getAppContext().authService, 'authCheck')
- .returns(Promise.resolve(true));
- element = new GrRestApiServiceImpl(
- getAppContext().authService,
- getAppContext().flagsService
- );
- element._projectLookup = {};
- });
-
- teardown(() => {
- window.CANONICAL_PATH = originalCanonicalPath;
- });
-
- test('parent diff comments are properly grouped', () => {
- sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(() =>
- Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- updated: '2017-02-03 22:32:28.000000000',
- message: 'this isn’t quite right',
- },
- {
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- })
- );
- return element
- ._getDiffComments('42', '', undefined, PARENT, 1, 'sieve.go')
- .then(obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- path: 'sieve.go',
- updated: '2017-02-03 22:33:28.000000000',
- });
- assert.equal(obj.comments.length, 1);
- assert.deepEqual(obj.comments[0], {
- message: 'this isn’t quite right',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- });
- });
-
- test('_setRange', () => {
- const comments = [
- {
- id: 1,
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ];
- const expectedResult = {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- };
- const comment = comments[1];
- assert.deepEqual(element._setRange(comments, comment), expectedResult);
- });
-
- test('_setRanges', () => {
- const comments = [
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- id: 1,
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- const expectedResult = [
- {
- id: 1,
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- assert.deepEqual(element._setRanges(comments), expectedResult);
- });
-
- test('differing patch diff comments are properly grouped', () => {
- sinon
- .stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(request => {
- const url = request.url;
- if (url === '/changes/test~42/revisions/1') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- });
- } else if (url === '/changes/test~42/revisions/2') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'What on earth are you thinking, here?',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: CommentSide.PARENT,
- message: 'Yeah not sure how this worked either?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- message: '¯\\_(ツ)_/¯',
- updated: '2017-02-04 22:33:28.000000000',
- },
- ],
- });
- }
- });
- return element
- ._getDiffComments('42', '', undefined, 1, 2, 'sieve.go')
- .then(obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
- message: 'this isn’t quite right',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- assert.equal(obj.comments.length, 2);
- assert.deepEqual(obj.comments[0], {
- message: 'What on earth are you thinking, here?',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- assert.deepEqual(obj.comments[1], {
- message: '¯\\_(ツ)_/¯',
- path: 'sieve.go',
- updated: '2017-02-04 22:33:28.000000000',
- });
- });
- });
-
- test('server error', () => {
- const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
- stubAuth('fetch').returns(Promise.resolve({ok: false}));
- const serverErrorEventPromise = new Promise(resolve => {
- addListenerForTest(document, 'server-error', resolve);
- });
-
- return Promise.all([
- element._restApiHelper.fetchJSON({}).then(response => {
- assert.isUndefined(response);
- assert.isTrue(getResponseObjectStub.notCalled);
- }),
- serverErrorEventPromise,
- ]);
- });
-
- test('legacy n,z key in change url is replaced', async () => {
- sinon.stub(element, 'getConfig').callsFake(async () => {
- return {};
- });
- const stub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([]));
- await element.getChanges(1, null, 'n,z');
- assert.equal(stub.lastCall.args[0].params.S, 0);
- });
-
- test('saveDiffPreferences invalidates cache line', () => {
- const cacheKey = '/accounts/self/preferences.diff';
- const sendStub = sinon.stub(element._restApiHelper, 'send');
- element._cache.set(cacheKey, {tab_size: 4});
- element.saveDiffPreferences({tab_size: 8});
- assert.isTrue(sendStub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- });
-
- suite('getAccountSuggestions', () => {
- let fetchStub;
- setup(() => {
- fetchStub = sinon
- .stub(element._restApiHelper, 'fetch')
- .returns(Promise.resolve(new Response()));
- });
-
- test('url with just email', () => {
- element.getSuggestedAccounts('bro');
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- 'test52/accounts/?o=DETAILS&q=bro'
- );
- });
-
- test('url with email and canSee changeId', () => {
- element.getSuggestedAccounts('bro', undefined, 341682);
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- 'test53/accounts/?o=DETAILS&q=bro%20and%20cansee%3A341682'
- );
- });
-
- test('url with email and canSee changeId and isActive', () => {
- element.getSuggestedAccounts('bro', undefined, 341682, true);
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- 'test54/accounts/?o=DETAILS&q=bro%20and%20' +
- 'cansee%3A341682%20and%20is%3Aactive'
- );
- });
- });
-
- test('getAccount when resp is null does not add to cache', async () => {
- const cacheKey = '/accounts/self/detail';
- const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve());
-
- await element.getAccount();
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
-
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn();
- });
-
- test('getAccount does not add to cache when status is 403', async () => {
- const cacheKey = '/accounts/self/detail';
- const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve());
-
- await element.getAccount();
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
-
- element._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn({status: 403});
- });
-
- test('getAccount when resp is successful', async () => {
- const cacheKey = '/accounts/self/detail';
- const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve());
-
- await element.getAccount();
-
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- assert.isTrue(stub.called);
- assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
- stub.lastCall.args[0].errFn({});
- });
-
- const preferenceSetup = function(testJSON, loggedIn) {
- sinon
- .stub(element, 'getLoggedIn')
- .callsFake(() => Promise.resolve(loggedIn));
- sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve(testJSON));
- };
-
- test('getPreferences returns correctly logged in', () => {
- const testJSON = {diff_view: 'SIDE_BY_SIDE'};
- const loggedIn = true;
-
- preferenceSetup(testJSON, loggedIn);
-
- return element.getPreferences().then(obj => {
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- });
- });
-
- test('getPreferences returns correctly on larger screens logged in', () => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = true;
-
- preferenceSetup(testJSON, loggedIn);
-
- return element.getPreferences().then(obj => {
- assert.equal(obj.diff_view, 'UNIFIED_DIFF');
- });
- });
-
- test('getPreferences returns correctly on larger screens no login', () => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = false;
-
- preferenceSetup(testJSON, loggedIn);
-
- return element.getPreferences().then(obj => {
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- });
- });
-
- test('savPreferences normalizes download scheme', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve(new Response()));
- element.savePreferences({download_scheme: 'HTTP'});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
- });
-
- test('getDiffPreferences returns correct defaults', () => {
- sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
-
- return element.getDiffPreferences().then(obj => {
- assert.equal(obj.context, 10);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.font_size, 12);
- assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.show_line_endings, true);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- });
- });
-
- test('saveDiffPreferences set show_tabs to false', () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send');
- element.saveDiffPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
-
- test('getEditPreferences returns correct defaults', () => {
- sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
-
- return element.getEditPreferences().then(obj => {
- assert.equal(obj.auto_close_brackets, false);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.hide_line_numbers, false);
- assert.equal(obj.hide_top_menu, false);
- assert.equal(obj.indent_unit, 2);
- assert.equal(obj.indent_with_tabs, false);
- assert.equal(obj.key_map_type, 'DEFAULT');
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.match_brackets, true);
- assert.equal(obj.show_base, false);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- assert.equal(obj.theme, 'DEFAULT');
- });
- });
-
- test('saveEditPreferences set show_tabs to false', () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send');
- element.saveEditPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
-
- test('confirmEmail', () => {
- const sendStub = sinon.spy(element._restApiHelper, 'send');
- element.confirmEmail('foo');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'PUT');
- assert.equal(sendStub.lastCall.args[0].url, '/config/server/email.confirm');
- assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
- });
-
- test('setAccountStatus', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve('OOO'));
- element._cache.set('/accounts/self/detail', {});
- return element.setAccountStatus('OOO').then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'PUT');
- assert.equal(sendStub.lastCall.args[0].url, '/accounts/self/status');
- assert.deepEqual(sendStub.lastCall.args[0].body, {status: 'OOO'});
- assert.deepEqual(
- element._restApiHelper._cache.get('/accounts/self/detail'),
- {status: 'OOO'}
- );
- });
- });
-
- suite('draft comments', () => {
- test('_sendDiffDraftRequest pending requests tracked', () => {
- const obj = element._pendingRequests;
- sinon
- .stub(element, '_getChangeURLAndSend')
- .callsFake(() => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 1);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 2);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- for (const promise of obj.sendDiffDraft) {
- promise.resolve();
- }
-
- return element.awaitPendingDiffDrafts().then(() => {
- assert.equal(obj.sendDiffDraft.length, 0);
- assert.isFalse(!!element.hasPendingDiffDrafts());
- });
- });
-
- suite('_failForCreate200', () => {
- test('_sendDiffDraftRequest checks for 200 on create', () => {
- const sendPromise = Promise.resolve();
- sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
- const failStub = sinon
- .stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
- assert.isTrue(failStub.calledOnce);
- assert.isTrue(failStub.calledWithExactly(sendPromise));
- });
- });
-
- test('_sendDiffDraftRequest no checks for 200 on non create', () => {
- sinon.stub(element, '_getChangeURLAndSend').returns(Promise.resolve());
- const failStub = sinon
- .stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element
- ._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
- .then(() => {
- assert.isFalse(failStub.called);
- });
- });
-
- test('_failForCreate200 fails on 200', () => {
- const result = {
- ok: true,
- status: 200,
- headers: {
- entries: () => [
- ['Set-CoOkiE', 'secret'],
- ['Innocuous', 'hello'],
- ],
- },
- };
- return element
- ._failForCreate200(Promise.resolve(result))
- .then(() => {
- assert.fail('Error expected.');
- })
- .catch(e => {
- assert.isOk(e);
- assert.include(e.message, 'Saving draft resulted in HTTP 200');
- assert.include(e.message, 'hello');
- assert.notInclude(e.message, 'secret');
- });
- });
-
- test('_failForCreate200 does not fail on 201', () => {
- const result = {
- ok: true,
- status: 201,
- headers: {entries: () => []},
- };
- return element._failForCreate200(Promise.resolve(result));
- });
- });
- });
-
- test('saveChangeEdit', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const change_num = '1';
- const file_name = 'index.php';
- const file_contents = '<?php';
- sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve([change_num, file_name, file_contents]));
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, file_name, file_contents]));
- element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
- return element
- .saveChangeEdit(change_num, file_name, file_contents)
- .then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].method,
- 'PUT'
- );
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/edit/' + file_name
- );
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].body,
- file_contents
- );
- });
- });
-
- test('putChangeCommitMessage', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const change_num = '1';
- const message = 'this is a commit message';
- sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve([change_num, message]));
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, message]));
- element._cache.set('/changes/' + change_num + '/message', {});
- return element.putChangeCommitMessage(change_num, message).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/message'
- );
- assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body, {
- message,
- });
- });
- });
-
- test('deleteChangeCommitMessage', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const change_num = '1';
- const messageId = 'abc';
- sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve([change_num, messageId]));
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, messageId]));
- return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].method,
- 'DELETE'
- );
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/messages/abc'
- );
- });
- });
-
- test('startWorkInProgress', () => {
- const sendStub = sinon
- .stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('ok'));
- element.startWorkInProgress('42');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {});
-
- element.startWorkInProgress('42', 'revising...');
- assert.isTrue(sendStub.calledTwice);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- message: 'revising...',
- });
- });
-
- test('deleteComment', () => {
- const sendStub = sinon
- .stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('some response'));
- return element
- .deleteComment('foo', 'bar', '01234', 'removal reason')
- .then(response => {
- assert.equal(response, 'some response');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
- assert.equal(
- sendStub.lastCall.args[0].endpoint,
- '/comments/01234/delete'
- );
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- reason: 'removal reason',
- });
- });
- });
-
- test('createRepo encodes name', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
- return element.createRepo({name: 'x/y'}).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
- });
- });
-
- test('queryChangeFiles', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.queryChangeFiles('42', EDIT, 'test/path.js').then(() => {
- assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
- assert.equal(
- fetchStub.lastCall.args[0].endpoint,
- '/files?q=test%2Fpath.js'
- );
- assert.equal(fetchStub.lastCall.args[0].revision, EDIT);
- });
- });
-
- test('normal use', () => {
- const defaultQuery = '';
-
- assert.equal(
- element._getReposUrl('test', 25).toString(),
- [false, '/projects/?n=26&S=0&d=&m=test'].toString()
- );
-
- assert.equal(
- element._getReposUrl(null, 25).toString(),
- [false, `/projects/?n=26&S=0&d=&m=${defaultQuery}`].toString()
- );
-
- assert.equal(
- element._getReposUrl('test', 25, 25).toString(),
- [false, '/projects/?n=26&S=25&d=&m=test'].toString()
- );
-
- assert.equal(
- element._getReposUrl('inname:test', 25, 25).toString(),
- [true, '/projects/?n=26&S=25&query=inname%3Atest'].toString()
- );
- });
-
- test('invalidateReposCache', () => {
- const url = '/projects/?n=26&S=0&query=test';
-
- element._cache.set(url, {});
-
- element.invalidateReposCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- test('invalidateAccountsCache', () => {
- const url = '/accounts/self/detail';
-
- element._cache.set(url, {});
-
- element.invalidateAccountsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getRepos', () => {
- const defaultQuery = '';
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .returns(Promise.resolve([]));
- });
-
- test('normal use', () => {
- element.getRepos('test', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=test'
- );
-
- element.getRepos(null, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&d=&m=${defaultQuery}`
- );
-
- element.getRepos('test', 25, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=25&d=&m=test'
- );
- });
-
- test('with blank', () => {
- element.getRepos('test/test', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=test%2Ftest'
- );
- });
-
- test('with hyphen', () => {
- element.getRepos('foo-bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo-bar'
- );
- });
-
- test('with leading hyphen', () => {
- element.getRepos('-bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=-bar'
- );
- });
-
- test('with trailing hyphen', () => {
- element.getRepos('foo-bar-', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo-bar-'
- );
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo_bar'
- );
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo_bar'
- );
- });
-
- test('hyphen only', () => {
- element.getRepos('-', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&d=&m=-`
- );
- });
-
- test('using query', () => {
- element.getRepos('description:project', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&query=description%3Aproject`
- );
- });
- });
-
- test('_getGroupsUrl normal use', () => {
- assert.equal(element._getGroupsUrl('test', 25), '/groups/?n=26&S=0&m=test');
-
- assert.equal(element._getGroupsUrl(null, 25), '/groups/?n=26&S=0');
-
- assert.equal(
- element._getGroupsUrl('test', 25, 25),
- '/groups/?n=26&S=25&m=test'
- );
- });
-
- test('invalidateGroupsCache', () => {
- const url = '/groups/?n=26&S=0&m=test';
-
- element._cache.set(url, {});
-
- element.invalidateGroupsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getGroups', () => {
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub = sinon.stub(element._restApiHelper, 'fetchCacheURL');
- });
-
- test('normal use', () => {
- element.getGroups('test', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&m=test'
- );
-
- element.getGroups(null, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url, '/groups/?n=26&S=0');
-
- element.getGroups('test', 25, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&m=test'
- );
- });
-
- test('regex', () => {
- element.getGroups('^test.*', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&r=%5Etest.*'
- );
-
- element.getGroups('^test.*', 25, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&r=%5Etest.*'
- );
- });
- });
-
- test('gerrit auth is used', () => {
- stubAuth('fetch').returns(Promise.resolve());
- element._restApiHelper.fetchJSON({url: 'foo'});
- assert(getAppContext().authService.fetch.called);
- });
-
- test('getSuggestedAccounts does not return _fetchJSON', () => {
- const _fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
- return element.getSuggestedAccounts().then(accts => {
- assert.isFalse(_fetchJSONSpy.called);
- assert.equal(accts.length, 0);
- });
- });
-
- test('_fetchJSON gets called by getSuggestedAccounts', () => {
- const _fetchJSONStub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .callsFake(() => Promise.resolve());
- return element.getSuggestedAccounts('own').then(() => {
- assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
- q: 'own',
- o: 'DETAILS',
- });
- });
- });
-
- suite('getChangeDetail', () => {
- suite('change detail options', () => {
- setup(() => {
- sinon
- .stub(element, '_getChangeDetail')
- .callsFake(async (changeNum, options) => {
- return {changeNum, options};
- });
- });
-
- test('signed pushes disabled', async () => {
- sinon.stub(element, 'getConfig').callsFake(async () => {
- return {};
- });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.isNotOk(
- parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
- );
- });
-
- test('signed pushes enabled', async () => {
- sinon.stub(element, 'getConfig').callsFake(async () => {
- return {receive: {enable_signed_push: true}};
- });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.ok(
- parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
- );
- });
- });
-
- test('GrReviewerUpdatesParser.parse is used', () => {
- sinon
- .stub(GrReviewerUpdatesParser, 'parse')
- .returns(Promise.resolve('foo'));
- return element.getChangeDetail(42).then(result => {
- assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
- assert.equal(result, 'foo');
- });
- });
-
- test('_getChangeDetail passes params to ETags decorator', () => {
- const changeNum = 4321;
- element._projectLookup[changeNum] = Promise.resolve('test');
- const expectedUrl =
- window.CANONICAL_PATH + '/changes/test~4321/detail?O=516714';
- sinon.stub(element._etags, 'getOptions');
- sinon.stub(element._etags, 'collect');
- return element._getChangeDetail(changeNum, '516714').then(() => {
- assert.isTrue(element._etags.getOptions.calledWithExactly(expectedUrl));
- assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
- });
- });
-
- test('_getChangeDetail calls errFn on 500', () => {
- const errFn = sinon.stub();
- sinon.stub(element, 'getChangeActionURL').returns(Promise.resolve(''));
- sinon
- .stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({ok: false, status: 500}));
- return element._getChangeDetail(123, '516714', errFn).then(() => {
- assert.isTrue(errFn.called);
- });
- });
-
- test('_getChangeDetail populates _projectLookup', async () => {
- sinon.stub(element, 'getChangeActionURL').returns(Promise.resolve(''));
- sinon.stub(element._restApiHelper, 'fetchRawJSON').returns(
- Promise.resolve({
- ok: true,
- status: 200,
- text: () => Promise.resolve(`)]}'{"_number":1,"project":"test"}`),
- })
- );
- await element._getChangeDetail(1, '516714');
- assert.equal(Object.keys(element._projectLookup).length, 1);
- const project = await element._projectLookup[1];
- assert.equal(project, 'test');
- });
-
- suite('_getChangeDetail ETag cache', () => {
- let requestUrl;
- let mockResponseSerial;
- let collectSpy;
-
- setup(() => {
- requestUrl = '/foo/bar';
- const mockResponse = {foo: 'bar', baz: 42};
- mockResponseSerial = JSON_PREFIX + JSON.stringify(mockResponse);
- sinon.stub(element._restApiHelper, 'urlWithParams').returns(requestUrl);
- sinon
- .stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(requestUrl));
- collectSpy = sinon.spy(element._etags, 'collect');
- });
-
- test('contributes to cache', () => {
- const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
- sinon.stub(element._restApiHelper, 'fetchRawJSON').returns(
- Promise.resolve({
- text: () => Promise.resolve(mockResponseSerial),
- status: 200,
- ok: true,
- })
- );
-
- return element._getChangeDetail(123, '516714').then(detail => {
- assert.isFalse(getPayloadSpy.called);
- assert.isTrue(collectSpy.calledOnce);
- const cachedResponse = element._etags.getCachedPayload(requestUrl);
- assert.equal(cachedResponse, mockResponseSerial);
- });
- });
-
- test('uses cache on HTTP 304', () => {
- const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
- getPayloadStub.returns(mockResponseSerial);
- sinon.stub(element._restApiHelper, 'fetchRawJSON').returns(
- Promise.resolve({
- text: () => Promise.resolve(''),
- status: 304,
- ok: true,
- })
- );
-
- return element._getChangeDetail(123, '').then(detail => {
- assert.isFalse(collectSpy.called);
- assert.isTrue(getPayloadStub.calledOnce);
- });
- });
- });
- });
-
- test('setInProjectLookup', async () => {
- await element.setInProjectLookup('test', 'project');
- const project = await element.getFromProjectLookup('test');
- assert.deepEqual(project, 'project');
- });
-
- suite('getFromProjectLookup', () => {
- test('getChange succeeds, no project', async () => {
- sinon.stub(element, 'getChange').returns(Promise.resolve(null));
- const val = await element.getFromProjectLookup();
- assert.strictEqual(val, undefined);
- });
-
- test('getChange succeeds with project', () => {
- sinon
- .stub(element, 'getChange')
- .returns(Promise.resolve({project: 'project'}));
- const projectLookup = element.getFromProjectLookup('test');
- return projectLookup.then(val => {
- assert.equal(val, 'project');
- assert.deepEqual(element._projectLookup, {test: projectLookup});
- });
- });
- });
-
- suite('getChanges populates _projectLookup', () => {
- test('multiple queries', async () => {
- sinon.stub(element._restApiHelper, 'fetchJSON').returns(
- Promise.resolve([
- [
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- ],
- [{_number: 3, project: 'test/test'}],
- ])
- );
- // When opt_query instanceof Array, _fetchJSON returns
- // Array<Array<Object>>.
- await element.getChangesForMultipleQueries(null, []);
- assert.equal(Object.keys(element._projectLookup).length, 3);
- const project1 = await element.getFromProjectLookup(1);
- assert.equal(project1, 'test');
- const project2 = await element.getFromProjectLookup(2);
- assert.equal(project2, 'test');
- const project3 = await element.getFromProjectLookup(3);
- assert.equal(project3, 'test/test');
- });
-
- test('no query', async () => {
- sinon.stub(element._restApiHelper, 'fetchJSON').returns(
- Promise.resolve([
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- {_number: 3, project: 'test/test'},
- ])
- );
-
- // When opt_query !instanceof Array, _fetchJSON returns
- // Array<Object>.
- await element.getChanges();
- assert.equal(Object.keys(element._projectLookup).length, 3);
- const project1 = await element.getFromProjectLookup(1);
- assert.equal(project1, 'test');
- const project2 = await element.getFromProjectLookup(2);
- assert.equal(project2, 'test');
- const project3 = await element.getFromProjectLookup(3);
- assert.equal(project3, 'test/test');
- });
- });
-
- test('getDetailedChangesWithActions', async () => {
- const c1 = createChange();
- c1._number = 1;
- const c2 = createChange();
- c2._number = 2;
- const getChangesStub = sinon
- .stub(element, 'getChanges')
- .callsFake((changesPerPage, query, offset, options) => {
- assert.isUndefined(changesPerPage);
- assert.strictEqual(query, 'change:1 OR change:2');
- assert.isUndefined(offset);
- assert.strictEqual(options, EXPECTED_QUERY_OPTIONS);
- return Promise.resolve([]);
- });
- await element.getDetailedChangesWithActions([c1._number, c2._number]);
- assert.isTrue(getChangesStub.calledOnce);
- });
-
- test('_getChangeURLAndFetch', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const fetchStub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve());
- const req = {changeNum: 1, endpoint: '/test', revision: 1};
- return element._getChangeURLAndFetch(req).then(() => {
- assert.equal(
- fetchStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test'
- );
- });
- });
-
- test('_getChangeURLAndSend', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
-
- const req = {
- changeNum: 1,
- method: 'POST',
- patchNum: 1,
- endpoint: '/test',
- };
- return element._getChangeURLAndSend(req).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(
- sendStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test'
- );
- });
- });
-
- suite('reading responses', () => {
- test('_readResponsePayload', async () => {
- const mockObject = {foo: 'bar', baz: 'foo'};
- const serial = JSON_PREFIX + JSON.stringify(mockObject);
- const mockResponse = {text: () => Promise.resolve(serial)};
- const payload = await readResponsePayload(mockResponse);
- assert.deepEqual(payload.parsed, mockObject);
- assert.equal(payload.raw, serial);
- });
-
- test('_parsePrefixedJSON', () => {
- const obj = {x: 3, y: {z: 4}, w: 23};
- const serial = JSON_PREFIX + JSON.stringify(obj);
- const result = parsePrefixedJSON(serial);
- assert.deepEqual(result, obj);
- });
- });
-
- test('setChangeTopic', () => {
- const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
- return element.setChangeTopic(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
- });
- });
-
- test('setChangeHashtag', () => {
- const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
- return element.setChangeHashtag(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
- });
- });
-
- test('generateAccountHttpPassword', () => {
- const sendSpy = sinon.spy(element._restApiHelper, 'send');
- return element.generateAccountHttpPassword().then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
- });
- });
-
- suite('getChangeFiles', () => {
- test('patch only', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: PARENT, patchNum: 2};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 2);
- assert.isNotOk(fetchStub.lastCall.args[0].params);
- });
- });
-
- test('simple range', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: 4, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: -3, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- suite('getDiff', () => {
- test('patchOnly', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, PARENT, 2, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 2);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- });
- });
-
- test('simple range', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- test('getDashboard', () => {
- const fetchCacheURLStub = sinon.stub(
- element._restApiHelper,
- 'fetchCacheURL'
- );
- element.getDashboard('gerrit/project', 'default:main');
- assert.isTrue(fetchCacheURLStub.calledOnce);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/gerrit%2Fproject/dashboards/default%3Amain'
- );
- });
-
- test('getFileContent', () => {
- sinon.stub(element, '_getChangeURLAndSend').returns(
- Promise.resolve({
- ok: 'true',
- headers: {
- get(header) {
- if (header === 'X-FYI-Content-Type') {
- return 'text/java';
- }
- },
- },
- })
- );
-
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve('new content'));
-
- const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
- assert.deepEqual(res, {
- content: 'new content',
- type: 'text/java',
- ok: true,
- });
- });
-
- const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
- assert.deepEqual(res, {
- content: 'new content',
- type: 'text/java',
- ok: true,
- });
- });
-
- return Promise.all([edit, normal]);
- });
-
- test('getFileContent suppresses 404s', () => {
- const res = {status: 404};
- const spy = sinon.spy();
- addListenerForTest(document, 'server-error', spy);
- sinon
- .stub(getAppContext().authService, 'fetch')
- .returns(Promise.resolve(res));
- sinon.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
- return element
- .getFileContent('1', 'tst/path', '1')
- .then(() => waitEventLoop())
- .then(() => {
- assert.isFalse(spy.called);
-
- res.status = 500;
- return element.getFileContent('1', 'tst/path', '1');
- })
- .then(() => {
- assert.isTrue(spy.called);
- assert.notEqual(spy.lastCall.args[0].detail.response.status, 404);
- });
- });
-
- test('getChangeFilesOrEditFiles is edit-sensitive', () => {
- const fn = element.getChangeOrEditFiles.bind(element);
- const getChangeFilesStub = sinon
- .stub(element, 'getChangeFiles')
- .returns(Promise.resolve({}));
- const getChangeEditFilesStub = sinon
- .stub(element, 'getChangeEditFiles')
- .returns(Promise.resolve({}));
-
- return fn('1', {patchNum: EDIT}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isFalse(getChangeFilesStub.called);
- return fn('1', {patchNum: '1'}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isTrue(getChangeFilesStub.calledOnce);
- });
- });
- });
-
- test('_fetch forwards request and logs', () => {
- const logStub = sinon.stub(element._restApiHelper, '_logCall');
- const response = {status: 404, text: sinon.stub()};
- const url = 'my url';
- const fetchOptions = {method: 'DELETE'};
- sinon.stub(element.authService, 'fetch').returns(Promise.resolve(response));
- const startTime = 123;
- sinon.stub(Date, 'now').returns(startTime);
- const req = {url, fetchOptions};
- return element._restApiHelper.fetch(req).then(() => {
- assert.isTrue(logStub.calledOnce);
- assert.isTrue(logStub.calledWith(req, startTime, response.status));
- assert.isFalse(response.text.called);
- });
- });
-
- test('_logCall only reports requests with anonymized URLss', async () => {
- sinon.stub(Date, 'now').returns(200);
- const handler = sinon.stub();
- addListenerForTest(document, 'gr-rpc-log', handler);
-
- element._restApiHelper._logCall({url: 'url'}, 100, 200);
- assert.isFalse(handler.called);
-
- element._restApiHelper._logCall(
- {url: 'url', anonymizedUrl: 'not url'},
- 100,
- 200
- );
- await waitEventLoop();
- assert.isTrue(handler.calledOnce);
- });
-
- test('ported comment errors do not trigger error dialog', () => {
- const change = createChange();
- const handler = sinon.stub();
- addListenerForTest(document, 'server-error', handler);
- sinon.stub(element._restApiHelper, 'fetchJSON').returns(
- Promise.resolve({
- ok: false,
- })
- );
-
- element.getPortedComments(change._number, CURRENT);
-
- assert.isFalse(handler.called);
- });
-
- test('ported drafts are not requested user is not logged in', () => {
- const change = createChange();
- sinon.stub(element, 'getLoggedIn').returns(Promise.resolve(false));
- const getChangeURLAndFetchStub = sinon.stub(
- element,
- '_getChangeURLAndFetch'
- );
-
- element.getPortedDrafts(change._number, CURRENT);
-
- assert.isFalse(getChangeURLAndFetchStub.called);
- });
-
- test('saveChangeStarred', async () => {
- sinon
- .stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
-
- await element.saveChangeStarred(123, true);
- assert.isTrue(sendStub.calledOnce);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'PUT',
- url: '/accounts/self/starred.changes/test~123',
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
-
- await element.saveChangeStarred(456, false);
- assert.isTrue(sendStub.calledTwice);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'DELETE',
- url: '/accounts/self/starred.changes/test~456',
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
- });
-});
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
new file mode 100644
index 0000000..37bc76e
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -0,0 +1,1588 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../test/common-test-setup';
+import {
+ addListenerForTest,
+ assertFails,
+ MockPromise,
+ mockPromise,
+ waitEventLoop,
+} from '../../test/test-utils';
+import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+import {
+ ListChangesOption,
+ listChangesOptionsToHex,
+} from '../../utils/change-util';
+import {
+ createAccountDetailWithId,
+ createChange,
+ createComment,
+ createParsedChange,
+ createServerInfo,
+} from '../../test/test-data-generators';
+import {CURRENT} from '../../utils/patch-set-util';
+import {
+ parsePrefixedJSON,
+ readResponsePayload,
+ JSON_PREFIX,
+} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {GrRestApiServiceImpl} from './gr-rest-api-impl';
+import {
+ CommentSide,
+ createDefaultEditPrefs,
+ HttpMethod,
+} from '../../constants/constants';
+import {
+ BasePatchSetNum,
+ ChangeMessageId,
+ CommentInfo,
+ DashboardId,
+ DiffPreferenceInput,
+ EDIT,
+ EditPreferencesInfo,
+ Hashtag,
+ HashtagsInput,
+ NumericChangeId,
+ PARENT,
+ ParsedJSON,
+ PatchSetNum,
+ PreferencesInfo,
+ RepoName,
+ RevisionId,
+ RevisionPatchSetNum,
+ RobotCommentInfo,
+ Timestamp,
+ UrlEncodedCommentId,
+} from '../../types/common';
+import {assert} from '@open-wc/testing';
+import {AuthService} from '../gr-auth/gr-auth';
+import {GrAuthMock} from '../gr-auth/gr-auth_mock';
+
+const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
+ ListChangesOption.CHANGE_ACTIONS,
+ ListChangesOption.CURRENT_ACTIONS,
+ ListChangesOption.CURRENT_REVISION,
+ ListChangesOption.DETAILED_LABELS,
+ ListChangesOption.SUBMIT_REQUIREMENTS
+);
+
+suite('gr-rest-api-service-impl tests', () => {
+ let element: GrRestApiServiceImpl;
+ let authService: AuthService;
+
+ let ctr = 0;
+ let originalCanonicalPath: string | undefined;
+
+ setup(() => {
+ // Modify CANONICAL_PATH to effectively reset cache.
+ ctr += 1;
+ originalCanonicalPath = window.CANONICAL_PATH;
+ window.CANONICAL_PATH = `test${ctr}`;
+
+ const testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sinon.stub(window, 'fetch').resolves(new Response(testJSON));
+ // fake auth
+ authService = new GrAuthMock();
+ sinon.stub(authService, 'authCheck').resolves(true);
+ element = new GrRestApiServiceImpl(authService);
+
+ element._projectLookup = {};
+ });
+
+ teardown(() => {
+ window.CANONICAL_PATH = originalCanonicalPath;
+ });
+
+ test('parent diff comments are properly grouped', async () => {
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ updated: '2017-02-03 22:32:28.000000000',
+ message: 'this isn’t quite right',
+ },
+ {
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ],
+ } as unknown as ParsedJSON);
+ const obj = await element._getDiffComments(
+ 42 as NumericChangeId,
+ '/comments',
+ undefined,
+ PARENT,
+ 1 as PatchSetNum,
+ 'sieve.go'
+ );
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ assert.equal(obj.comments.length, 1);
+ assert.deepEqual(obj.comments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ });
+
+ test('_setRange', () => {
+ const comments: CommentInfo[] = [
+ {
+ id: '1' as UrlEncodedCommentId,
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ },
+ ];
+ const expectedResult: CommentInfo = {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ };
+ const comment = comments[1];
+ assert.deepEqual(element._setRange(comments, comment), expectedResult);
+ });
+
+ test('_setRanges', () => {
+ const comments: CommentInfo[] = [
+ {
+ id: '3' as UrlEncodedCommentId,
+ in_reply_to: '2' as UrlEncodedCommentId,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000' as Timestamp,
+ },
+ {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ },
+ {
+ id: '1' as UrlEncodedCommentId,
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ const expectedResult: CommentInfo[] = [
+ {
+ id: '1' as UrlEncodedCommentId,
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: '3' as UrlEncodedCommentId,
+ in_reply_to: '2' as UrlEncodedCommentId,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ assert.deepEqual(element._setRanges(comments), expectedResult);
+ });
+
+ test('differing patch diff comments are properly grouped', async () => {
+ sinon.stub(element, 'getFromProjectLookup').resolves('test' as RepoName);
+ sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(async request => {
+ const url = request.url;
+ if (url === '/changes/test~42/revisions/1/comments') {
+ return {
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:32:28.000000000',
+ },
+ {
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ],
+ } as unknown as ParsedJSON;
+ } else if (url === '/changes/test~42/revisions/2/comments') {
+ return {
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ message: 'What on earth are you thinking, here?',
+ updated: '2017-02-03 22:32:28.000000000',
+ },
+ {
+ side: CommentSide.PARENT,
+ message: 'Yeah not sure how this worked either?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ {
+ message: '¯\\_(ツ)_/¯',
+ updated: '2017-02-04 22:33:28.000000000',
+ },
+ ],
+ } as unknown as ParsedJSON;
+ }
+ return undefined;
+ });
+ const obj = await element._getDiffComments(
+ 42 as NumericChangeId,
+ '/comments',
+ undefined,
+ 1 as BasePatchSetNum,
+ 2 as PatchSetNum,
+ 'sieve.go'
+ );
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ assert.equal(obj.comments.length, 2);
+ assert.deepEqual(obj.comments[0], {
+ message: 'What on earth are you thinking, here?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ assert.deepEqual(obj.comments[1], {
+ message: '¯\\_(ツ)_/¯',
+ path: 'sieve.go',
+ updated: '2017-02-04 22:33:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ });
+
+ test('server error', async () => {
+ const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
+ sinon
+ .stub(authService, 'fetch')
+ .resolves(new Response(undefined, {status: 502}));
+ const serverErrorEventPromise = new Promise(resolve => {
+ addListenerForTest(document, 'server-error', resolve);
+ });
+ const response = await element._restApiHelper.fetchJSON({url: ''});
+ assert.isUndefined(response);
+ assert.isTrue(getResponseObjectStub.notCalled);
+ await serverErrorEventPromise;
+ });
+
+ test('legacy n,z key in change url is replaced', async () => {
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves([] as unknown as ParsedJSON);
+ await element.getChanges(1, undefined, 'n,z');
+ assert.equal(stub.lastCall.args[0].params!.S, 0);
+ });
+
+ test('saveDiffPreferences invalidates cache line', () => {
+ const cacheKey = '/accounts/self/preferences.diff';
+ const sendStub = sinon.stub(element._restApiHelper, 'send');
+ element._cache.set(cacheKey, {tab_size: 4} as unknown as ParsedJSON);
+ element.saveDiffPreferences({
+ tab_size: 8,
+ ignore_whitespace: 'IGNORE_NONE',
+ });
+ assert.isTrue(sendStub.called);
+ assert.isFalse(element._cache.has(cacheKey));
+ });
+
+ suite('getAccountSuggestions', () => {
+ let fetchStub: sinon.SinonStub;
+ setup(() => {
+ fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response());
+ });
+
+ test('url with just email', () => {
+ element.getSuggestedAccounts('bro');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.firstCall.args[0].url,
+ 'test52/accounts/?o=DETAILS&q=%22bro%22'
+ );
+ });
+
+ test('url with email and canSee changeId', () => {
+ element.getSuggestedAccounts('bro', undefined, 341682 as NumericChangeId);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.firstCall.args[0].url,
+ 'test53/accounts/?o=DETAILS&q=%22bro%22%20and%20cansee%3A341682'
+ );
+ });
+
+ test('url with email and canSee changeId and isActive', () => {
+ element.getSuggestedAccounts(
+ 'bro',
+ undefined,
+ 341682 as NumericChangeId,
+ true
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.firstCall.args[0].url,
+ 'test54/accounts/?o=DETAILS&q=%22bro%22%20and%20' +
+ 'cansee%3A341682%20and%20is%3Aactive'
+ );
+ });
+ });
+
+ test('getAccount when resp is undefined clears cache', async () => {
+ const cacheKey = '/accounts/self/detail';
+ const account = createAccountDetailWithId();
+ element._cache.set(cacheKey, account);
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(async req => {
+ req.errFn!(undefined);
+ return undefined;
+ });
+ assert.isTrue(element._cache.has(cacheKey));
+
+ await element.getAccount();
+ assert.isTrue(stub.called);
+ assert.isFalse(element._cache.has(cacheKey));
+ });
+
+ test('getAccount when status is 403 clears cache', async () => {
+ const cacheKey = '/accounts/self/detail';
+ const account = createAccountDetailWithId();
+ element._cache.set(cacheKey, account);
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(async req => {
+ req.errFn!(new Response(undefined, {status: 403}));
+ return undefined;
+ });
+ assert.isTrue(element._cache.has(cacheKey));
+
+ await element.getAccount();
+ assert.isTrue(stub.called);
+ assert.isFalse(element._cache.has(cacheKey));
+ });
+
+ test('getAccount when resp is successful updates cache', async () => {
+ const cacheKey = '/accounts/self/detail';
+ const account = createAccountDetailWithId();
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(async () => {
+ element._cache.set(cacheKey, account);
+ return undefined;
+ });
+ assert.isFalse(element._cache.has(cacheKey));
+
+ await element.getAccount();
+ assert.isTrue(stub.called);
+ assert.equal(element._cache.get(cacheKey), account);
+ });
+
+ const preferenceSetup = function (testJSON: unknown, loggedIn: boolean) {
+ sinon
+ .stub(element, 'getLoggedIn')
+ .callsFake(() => Promise.resolve(loggedIn));
+ sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(() => Promise.resolve(testJSON as ParsedJSON));
+ };
+
+ test('getPreferences returns correctly logged in', async () => {
+ const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+ const loggedIn = true;
+
+ preferenceSetup(testJSON, loggedIn);
+
+ const obj = await element.getPreferences();
+ assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
+ });
+
+ test('getPreferences returns correctly on larger screens logged in', async () => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = true;
+
+ preferenceSetup(testJSON, loggedIn);
+
+ const obj = await element.getPreferences();
+ assert.equal(obj!.diff_view, 'UNIFIED_DIFF');
+ });
+
+ test('getPreferences returns correctly on larger screens no login', async () => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = false;
+
+ preferenceSetup(testJSON, loggedIn);
+
+ const obj = await element.getPreferences();
+ assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
+ });
+
+ test('savPreferences normalizes download scheme', () => {
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves(new Response());
+ element.savePreferences({download_scheme: 'HTTP'});
+ assert.isTrue(sendStub.called);
+ assert.equal(
+ (sendStub.lastCall.args[0].body as Partial<PreferencesInfo>)
+ .download_scheme,
+ 'http'
+ );
+ });
+
+ test('getDiffPreferences returns correct defaults', async () => {
+ sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
+
+ const obj = (await element.getDiffPreferences())!;
+ assert.equal(obj.context, 10);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.font_size, 12);
+ assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.show_line_endings, true);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ });
+
+ test('saveDiffPreferences set show_tabs to false', () => {
+ const sendStub = sinon.stub(element._restApiHelper, 'send');
+ element.saveDiffPreferences({
+ show_tabs: false,
+ ignore_whitespace: 'IGNORE_NONE',
+ });
+ assert.isTrue(sendStub.called);
+ assert.equal(
+ (sendStub.lastCall.args[0].body as Partial<DiffPreferenceInput>)
+ .show_tabs,
+ false
+ );
+ });
+
+ test('getEditPreferences returns correct defaults', async () => {
+ sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
+
+ const obj = (await element.getEditPreferences())!;
+ assert.equal(obj.auto_close_brackets, false);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.hide_line_numbers, false);
+ assert.equal(obj.hide_top_menu, false);
+ assert.equal(obj.indent_unit, 2);
+ assert.equal(obj.indent_with_tabs, false);
+ assert.equal(obj.key_map_type, 'DEFAULT');
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.match_brackets, true);
+ assert.equal(obj.show_base, false);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ assert.equal(obj.theme, 'DEFAULT');
+ });
+
+ test('saveEditPreferences set show_tabs to false', () => {
+ const sendStub = sinon.stub(element._restApiHelper, 'send');
+ element.saveEditPreferences({
+ ...createDefaultEditPrefs(),
+ show_tabs: false,
+ });
+ assert.isTrue(sendStub.called);
+ assert.equal(
+ (sendStub.lastCall.args[0].body as EditPreferencesInfo).show_tabs,
+ false
+ );
+ });
+
+ test('confirmEmail', () => {
+ const sendStub = sinon.spy(element._restApiHelper, 'send');
+ element.confirmEmail('foo');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(sendStub.lastCall.args[0].url, '/config/server/email.confirm');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
+ });
+
+ test('setAccountStatus', async () => {
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves('OOO' as unknown as ParsedJSON);
+ element._cache.set('/accounts/self/detail', createAccountDetailWithId());
+ await element.setAccountStatus('OOO');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(sendStub.lastCall.args[0].url, '/accounts/self/status');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {status: 'OOO'});
+ assert.deepEqual(
+ element._cache.get('/accounts/self/detail')!.status,
+ 'OOO'
+ );
+ });
+
+ suite('draft comments', () => {
+ test('_sendDiffDraftRequest pending requests tracked', async () => {
+ const obj = element._pendingRequests;
+ sinon
+ .stub(element, '_getChangeURLAndSend')
+ .callsFake(() => mockPromise());
+ assert.notOk(element.hasPendingDiffDrafts());
+
+ element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 1 as PatchSetNum,
+ {}
+ );
+ assert.equal(obj.sendDiffDraft.length, 1);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
+
+ element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 1 as PatchSetNum,
+ {}
+ );
+ assert.equal(obj.sendDiffDraft.length, 2);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
+
+ for (const promise of obj.sendDiffDraft) {
+ (promise as MockPromise<void>).resolve();
+ }
+
+ await element.awaitPendingDiffDrafts();
+ assert.equal(obj.sendDiffDraft.length, 0);
+ assert.isFalse(!!element.hasPendingDiffDrafts());
+ });
+
+ suite('_failForCreate200', () => {
+ test('_sendDiffDraftRequest checks for 200 on create', async () => {
+ const sendPromise = Promise.resolve({} as unknown as ParsedJSON);
+ sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
+ const failStub = sinon.stub(element, '_failForCreate200').resolves();
+ await element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 4 as PatchSetNum,
+ {}
+ );
+ assert.isTrue(failStub.calledOnce);
+ assert.isTrue(failStub.calledWithExactly(sendPromise));
+ });
+
+ test('_sendDiffDraftRequest no checks for 200 on non create', async () => {
+ sinon.stub(element, '_getChangeURLAndSend').resolves();
+ const failStub = sinon.stub(element, '_failForCreate200').resolves();
+ await element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 4 as PatchSetNum,
+ {
+ id: '123' as UrlEncodedCommentId,
+ }
+ );
+ assert.isFalse(failStub.called);
+ });
+
+ test('_failForCreate200 fails on 200', async () => {
+ const result = new Response(undefined, {
+ status: 200,
+ headers: {
+ 'Set-CoOkiE': 'secret',
+ Innocuous: 'hello',
+ },
+ });
+ const error = (await assertFails(
+ element._failForCreate200(Promise.resolve(result))
+ )) as Error;
+ assert.isOk(error);
+ assert.include(error.message, 'Saving draft resulted in HTTP 200');
+ assert.include(error.message, 'hello');
+ assert.notInclude(error.message, 'secret');
+ });
+
+ test('_failForCreate200 does not fail on 201', () => {
+ const result = new Response(undefined, {status: 201});
+ return element._failForCreate200(Promise.resolve(result));
+ });
+ });
+ });
+
+ test('saveChangeEdit', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const file_name = 'index.php';
+ const file_contents = '<?php';
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves([
+ change_num,
+ file_name,
+ file_contents,
+ ] as unknown as ParsedJSON);
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves([
+ change_num,
+ file_name,
+ file_contents,
+ ] as unknown as ParsedJSON);
+ element._cache.set(
+ `/changes/${change_num}/edit/${file_name}`,
+ {} as unknown as ParsedJSON
+ );
+ await element.saveChangeEdit(change_num, file_name, file_contents);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(
+ sendStub.lastCall.args[0].url,
+ '/changes/test~1/edit/' + file_name
+ );
+ assert.equal(sendStub.lastCall.args[0].body, file_contents);
+ });
+
+ test('putChangeCommitMessage', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const message = 'this is a commit message';
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves([change_num, message] as unknown as ParsedJSON);
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves([change_num, message] as unknown as ParsedJSON);
+ element._cache.set(
+ `/changes/${change_num}/message`,
+ {} as unknown as ParsedJSON
+ );
+ await element.putChangeCommitMessage(change_num, message);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(sendStub.lastCall.args[0].url, '/changes/test~1/message');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {
+ message,
+ });
+ });
+
+ test('deleteChangeCommitMessage', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const messageId = 'abc' as ChangeMessageId;
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves([change_num, messageId] as unknown as ParsedJSON);
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves([change_num, messageId] as unknown as ParsedJSON);
+ await element.deleteChangeCommitMessage(change_num, messageId);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.DELETE);
+ assert.equal(sendStub.lastCall.args[0].url, '/changes/test~1/messages/abc');
+ });
+
+ test('startWorkInProgress', () => {
+ const sendStub = sinon
+ .stub(element, '_getChangeURLAndSend')
+ .resolves('ok' as unknown as ParsedJSON);
+ element.startWorkInProgress(42 as NumericChangeId);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {});
+
+ element.startWorkInProgress(42 as NumericChangeId, 'revising...');
+ assert.isTrue(sendStub.calledTwice);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {
+ message: 'revising...',
+ });
+ });
+
+ test('deleteComment', async () => {
+ const comment = createComment();
+ const sendStub = sinon
+ .stub(element, '_getChangeURLAndSend')
+ .resolves(comment as unknown as ParsedJSON);
+ const response = await element.deleteComment(
+ 123 as NumericChangeId,
+ 1 as PatchSetNum,
+ '01234' as UrlEncodedCommentId,
+ 'removal reason'
+ );
+ assert.equal(response, comment);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 123 as NumericChangeId);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.equal(sendStub.lastCall.args[0].patchNum, 1 as PatchSetNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/comments/01234/delete');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {
+ reason: 'removal reason',
+ });
+ });
+
+ test('createRepo encodes name', async () => {
+ const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+ await element.createRepo({name: 'x/y' as RepoName});
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ });
+
+ test('queryChangeFiles', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.queryChangeFiles(42 as NumericChangeId, EDIT, 'test/path.js');
+ assert.equal(fetchStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
+ assert.equal(
+ fetchStub.lastCall.args[0].endpoint,
+ '/files?q=test%2Fpath.js'
+ );
+ assert.equal(fetchStub.lastCall.args[0].revision, EDIT);
+ });
+
+ test('normal use', () => {
+ const defaultQuery = '';
+
+ assert.equal(
+ element._getReposUrl('test', 25).toString(),
+ [false, '/projects/?n=26&S=0&d=&m=test'].toString()
+ );
+
+ assert.equal(
+ element._getReposUrl(undefined, 25).toString(),
+ [false, `/projects/?n=26&S=0&d=&m=${defaultQuery}`].toString()
+ );
+
+ assert.equal(
+ element._getReposUrl('test', 25, 25).toString(),
+ [false, '/projects/?n=26&S=25&d=&m=test'].toString()
+ );
+
+ assert.equal(
+ element._getReposUrl('inname:test', 25, 25).toString(),
+ [true, '/projects/?n=26&S=25&query=inname%3Atest'].toString()
+ );
+ });
+
+ test('invalidateReposCache', () => {
+ const url = '/projects/?n=26&S=0&query=test';
+
+ element._cache.set(url, {} as unknown as ParsedJSON);
+
+ element.invalidateReposCache();
+
+ assert.isUndefined(element._sharedFetchPromises.get(url));
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ test('invalidateAccountsCache', () => {
+ const url = '/accounts/self/detail';
+
+ element._cache.set(url, {} as unknown as ParsedJSON);
+
+ element.invalidateAccountsCache();
+
+ assert.isUndefined(element._sharedFetchPromises.get(url));
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getRepos', () => {
+ const defaultQuery = '';
+ let fetchCacheURLStub: sinon.SinonStub;
+ setup(() => {
+ fetchCacheURLStub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .resolves([] as unknown as ParsedJSON);
+ });
+
+ test('normal use', () => {
+ element.getRepos('test', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=test'
+ );
+
+ element.getRepos(undefined, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ `/projects/?n=26&S=0&d=&m=${defaultQuery}`
+ );
+
+ element.getRepos('test', 25, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=25&d=&m=test'
+ );
+ });
+
+ test('with blank', () => {
+ element.getRepos('test/test', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=test%2Ftest'
+ );
+ });
+
+ test('with hyphen', () => {
+ element.getRepos('foo-bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo-bar'
+ );
+ });
+
+ test('with leading hyphen', () => {
+ element.getRepos('-bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=-bar'
+ );
+ });
+
+ test('with trailing hyphen', () => {
+ element.getRepos('foo-bar-', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo-bar-'
+ );
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo_bar'
+ );
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo_bar'
+ );
+ });
+
+ test('hyphen only', () => {
+ element.getRepos('-', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=-'
+ );
+ });
+
+ test('using query', () => {
+ element.getRepos('description:project', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=description%3Aproject'
+ );
+ });
+ });
+
+ test('_getGroupsUrl normal use', () => {
+ assert.equal(element._getGroupsUrl('test', 25), '/groups/?n=26&S=0&m=test');
+
+ assert.equal(element._getGroupsUrl('', 25), '/groups/?n=26&S=0');
+
+ assert.equal(
+ element._getGroupsUrl('test', 25, 25),
+ '/groups/?n=26&S=25&m=test'
+ );
+ });
+
+ test('invalidateGroupsCache', () => {
+ const url = '/groups/?n=26&S=0&m=test';
+
+ element._cache.set(url, {} as unknown as ParsedJSON);
+
+ element.invalidateGroupsCache();
+
+ assert.isUndefined(element._sharedFetchPromises.get(url));
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getGroups', () => {
+ let fetchCacheURLStub: sinon.SinonStub;
+ setup(() => {
+ fetchCacheURLStub = sinon.stub(element._restApiHelper, 'fetchCacheURL');
+ });
+
+ test('normal use', () => {
+ element.getGroups('test', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&m=test'
+ );
+
+ element.getGroups('', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url, '/groups/?n=26&S=0');
+
+ element.getGroups('test', 25, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&m=test'
+ );
+ });
+
+ test('regex', () => {
+ element.getGroups('^test.*', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&r=%5Etest.*'
+ );
+
+ element.getGroups('^test.*', 25, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&r=%5Etest.*'
+ );
+ });
+ });
+
+ test('gerrit auth is used', () => {
+ const fetchStub = sinon.stub(authService, 'fetch').resolves();
+ element._restApiHelper.fetchJSON({url: 'foo'});
+ assert(fetchStub.called);
+ });
+
+ test('getSuggestedAccounts does not return fetchJSON', async () => {
+ const fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
+ const accts = await element.getSuggestedAccounts('');
+ assert.isFalse(fetchJSONSpy.called);
+ assert.equal(accts!.length, 0);
+ });
+
+ test('fetchJSON gets called by getSuggestedAccounts', async () => {
+ const fetchJSONStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
+ await element.getSuggestedAccounts('own');
+ assert.deepEqual(fetchJSONStub.lastCall.args[0].params, {
+ q: '"own"',
+ o: 'DETAILS',
+ });
+ });
+
+ suite('getChangeDetail', () => {
+ suite('change detail options', () => {
+ let changeDetailStub: sinon.SinonStub;
+ setup(() => {
+ changeDetailStub = sinon
+ .stub(element, '_getChangeDetail')
+ .resolves({...createChange(), _number: 123 as NumericChangeId});
+ });
+
+ test('signed pushes disabled', async () => {
+ sinon.stub(element, 'getConfig').resolves({
+ ...createServerInfo(),
+ receive: {enable_signed_push: undefined},
+ });
+ const change = await element.getChangeDetail(123 as NumericChangeId);
+ assert.strictEqual(123, change!._number);
+ const options = changeDetailStub.firstCall.args[1];
+ assert.isNotOk(
+ parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
+ );
+ });
+
+ test('signed pushes enabled', async () => {
+ sinon.stub(element, 'getConfig').resolves({
+ ...createServerInfo(),
+ receive: {enable_signed_push: 'true'},
+ });
+ const change = await element.getChangeDetail(123 as NumericChangeId);
+ assert.strictEqual(123, change!._number);
+ const options = changeDetailStub.firstCall.args[1];
+ assert.ok(
+ parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
+ );
+ });
+ });
+
+ test('GrReviewerUpdatesParser.parse is used', async () => {
+ const changeInfo = createParsedChange();
+ const parseStub = sinon
+ .stub(GrReviewerUpdatesParser, 'parse')
+ .resolves(changeInfo);
+ const result = await element.getChangeDetail(42 as NumericChangeId);
+ assert.isTrue(parseStub.calledOnce);
+ assert.equal(result, changeInfo);
+ });
+
+ test('_getChangeDetail passes params to ETags decorator', async () => {
+ const changeNum = 4321 as NumericChangeId;
+ element._projectLookup[changeNum] = Promise.resolve('test' as RepoName);
+ const expectedUrl = `${window.CANONICAL_PATH}/changes/test~4321/detail?O=516714`;
+ const optionsStub = sinon.stub(element._etags, 'getOptions');
+ const collectStub = sinon.stub(element._etags, 'collect');
+ await element._getChangeDetail(changeNum, '516714');
+ assert.isTrue(optionsStub.calledWithExactly(expectedUrl));
+ assert.equal(collectStub.lastCall.args[0], expectedUrl);
+ });
+
+ test('_getChangeDetail calls errFn on 500', async () => {
+ const errFn = sinon.stub();
+ sinon.stub(element, 'getChangeActionURL').resolves('');
+ sinon
+ .stub(element._restApiHelper, 'fetchRawJSON')
+ .resolves(new Response(undefined, {status: 500}));
+ await element._getChangeDetail(123 as NumericChangeId, '516714', errFn);
+ assert.isTrue(errFn.called);
+ });
+
+ test('_getChangeDetail populates _projectLookup', async () => {
+ sinon.stub(element, 'getChangeActionURL').resolves('');
+ sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ new Response(')]}\'{"_number":1,"project":"test"}', {
+ status: 200,
+ })
+ );
+ await element._getChangeDetail(1 as NumericChangeId, '516714');
+ assert.equal(Object.keys(element._projectLookup).length, 1);
+ const project = await element._projectLookup[1];
+ assert.equal(project, 'test' as RepoName);
+ });
+
+ suite('_getChangeDetail ETag cache', () => {
+ let requestUrl: string;
+ let mockResponseSerial: string;
+ let collectSpy: sinon.SinonSpy;
+
+ setup(() => {
+ requestUrl = '/foo/bar';
+ const mockResponse = {foo: 'bar', baz: 42};
+ mockResponseSerial = JSON_PREFIX + JSON.stringify(mockResponse);
+ sinon.stub(element._restApiHelper, 'urlWithParams').returns(requestUrl);
+ sinon.stub(element, 'getChangeActionURL').resolves(requestUrl);
+ collectSpy = sinon.spy(element._etags, 'collect');
+ });
+
+ test('contributes to cache', async () => {
+ const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
+ sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ new Response(mockResponseSerial, {
+ status: 200,
+ })
+ );
+
+ await element._getChangeDetail(123 as NumericChangeId, '516714');
+ assert.isFalse(getPayloadSpy.called);
+ assert.isTrue(collectSpy.calledOnce);
+ const cachedResponse = element._etags.getCachedPayload(requestUrl);
+ assert.equal(cachedResponse, mockResponseSerial);
+ });
+
+ test('uses cache on HTTP 304', async () => {
+ const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
+ getPayloadStub.returns(mockResponseSerial);
+ sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ new Response(undefined, {
+ status: 304,
+ })
+ );
+
+ await element._getChangeDetail(123 as NumericChangeId, '');
+ assert.isFalse(collectSpy.called);
+ assert.isTrue(getPayloadStub.calledOnce);
+ });
+ });
+ });
+
+ test('setInProjectLookup', async () => {
+ await element.setInProjectLookup(
+ 555 as NumericChangeId,
+ 'project' as RepoName
+ );
+ const project = await element.getFromProjectLookup(555 as NumericChangeId);
+ assert.deepEqual(project, 'project' as RepoName);
+ });
+
+ suite('getFromProjectLookup', () => {
+ test('getChange succeeds, no project', async () => {
+ sinon.stub(element, 'getChange').resolves(null);
+ const val = await element.getFromProjectLookup(555 as NumericChangeId);
+ assert.strictEqual(val, undefined);
+ });
+
+ test('getChange succeeds with project', async () => {
+ sinon
+ .stub(element, 'getChange')
+ .resolves({...createChange(), project: 'project' as RepoName});
+ const projectLookup = element.getFromProjectLookup(
+ 555 as NumericChangeId
+ );
+ const val = await projectLookup;
+ assert.equal(val, 'project' as RepoName);
+ assert.deepEqual(element._projectLookup, {'555': projectLookup});
+ });
+ });
+
+ suite('getChanges populates _projectLookup', () => {
+ test('multiple queries', async () => {
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves([
+ [
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ ],
+ [{_number: 3, project: 'test/test'}],
+ ] as unknown as ParsedJSON);
+ // When opt_query instanceof Array, fetchJSON returns
+ // Array<Array<Object>>.
+ await element.getChangesForMultipleQueries(undefined, []);
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ const project1 = await element.getFromProjectLookup(1 as NumericChangeId);
+ assert.equal(project1, 'test' as RepoName);
+ const project2 = await element.getFromProjectLookup(2 as NumericChangeId);
+ assert.equal(project2, 'test' as RepoName);
+ const project3 = await element.getFromProjectLookup(3 as NumericChangeId);
+ assert.equal(project3, 'test/test' as RepoName);
+ });
+
+ test('no query', async () => {
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves([
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ {_number: 3, project: 'test/test'},
+ ] as unknown as ParsedJSON);
+
+ // When opt_query !instanceof Array, fetchJSON returns Array<Object>.
+ await element.getChanges();
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ const project1 = await element.getFromProjectLookup(1 as NumericChangeId);
+ assert.equal(project1, 'test' as RepoName);
+ const project2 = await element.getFromProjectLookup(2 as NumericChangeId);
+ assert.equal(project2, 'test' as RepoName);
+ const project3 = await element.getFromProjectLookup(3 as NumericChangeId);
+ assert.equal(project3, 'test/test' as RepoName);
+ });
+ });
+
+ test('getDetailedChangesWithActions', async () => {
+ const c1 = createChange();
+ c1._number = 1 as NumericChangeId;
+ const c2 = createChange();
+ c2._number = 2 as NumericChangeId;
+ const getChangesStub = sinon
+ .stub(element, 'getChanges')
+ .callsFake((changesPerPage, query, offset, options) => {
+ assert.isUndefined(changesPerPage);
+ assert.strictEqual(query, 'change:1 OR change:2');
+ assert.isUndefined(offset);
+ assert.strictEqual(options, EXPECTED_QUERY_OPTIONS);
+ return Promise.resolve([]);
+ });
+ await element.getDetailedChangesWithActions([c1._number, c2._number]);
+ assert.isTrue(getChangesStub.calledOnce);
+ });
+
+ test('_getChangeURLAndFetch', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
+ const req = {
+ changeNum: 1 as NumericChangeId,
+ endpoint: '/test',
+ revision: 1 as RevisionId,
+ };
+ await element._getChangeURLAndFetch(req);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test'
+ );
+ });
+
+ test('_getChangeURLAndSend', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+
+ const req = {
+ changeNum: 1 as NumericChangeId,
+ method: HttpMethod.POST,
+ patchNum: 1 as PatchSetNum,
+ endpoint: '/test',
+ };
+ await element._getChangeURLAndSend(req);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.equal(
+ sendStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test'
+ );
+ });
+
+ suite('reading responses', () => {
+ test('_readResponsePayload', async () => {
+ const mockObject = {foo: 'bar', baz: 'foo'} as unknown as ParsedJSON;
+ const serial = JSON_PREFIX + JSON.stringify(mockObject);
+ const response = new Response(serial);
+ const payload = await readResponsePayload(response);
+ assert.deepEqual(payload.parsed, mockObject);
+ assert.equal(payload.raw, serial);
+ });
+
+ test('_parsePrefixedJSON', () => {
+ const obj = {x: 3, y: {z: 4}, w: 23} as unknown as ParsedJSON;
+ const serial = JSON_PREFIX + JSON.stringify(obj);
+ const result = parsePrefixedJSON(serial);
+ assert.deepEqual(result, obj);
+ });
+ });
+
+ test('setChangeTopic', async () => {
+ const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
+ await element.setChangeTopic(123 as NumericChangeId, 'foo-bar');
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
+ });
+
+ test('setChangeHashtag', async () => {
+ const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
+ await element.setChangeHashtag(123 as NumericChangeId, {
+ add: ['foo-bar' as Hashtag],
+ });
+ assert.isTrue(sendSpy.calledOnce);
+ assert.sameDeepMembers(
+ (sendSpy.lastCall.args[0].body! as HashtagsInput).add!,
+ ['foo-bar']
+ );
+ });
+
+ test('generateAccountHttpPassword', async () => {
+ const sendSpy = sinon.spy(element._restApiHelper, 'send');
+ await element.generateAccountHttpPassword();
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
+ });
+
+ suite('getChangeFiles', () => {
+ test('patch only', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ const range = {basePatchNum: PARENT, patchNum: 2 as RevisionPatchSetNum};
+ await element.getChangeFiles(123 as NumericChangeId, range);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].revision,
+ 2 as RevisionPatchSetNum
+ );
+ assert.isNotOk(fetchStub.lastCall.args[0].params);
+ });
+
+ test('simple range', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ const range = {
+ basePatchNum: 4 as BasePatchSetNum,
+ patchNum: 5 as RevisionPatchSetNum,
+ };
+ await element.getChangeFiles(123 as NumericChangeId, range);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+ });
+
+ test('parent index', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ const range = {
+ basePatchNum: -3 as BasePatchSetNum,
+ patchNum: 5 as RevisionPatchSetNum,
+ };
+ await element.getChangeFiles(123 as NumericChangeId, range);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+ assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
+ });
+ });
+
+ suite('getDiff', () => {
+ test('patchOnly', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.getDiff(
+ 123 as NumericChangeId,
+ PARENT,
+ 2 as PatchSetNum,
+ 'foo/bar.baz'
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 2 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+ });
+
+ test('simple range', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.getDiff(
+ 123 as NumericChangeId,
+ 4 as PatchSetNum,
+ 5 as PatchSetNum,
+ 'foo/bar.baz'
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+ assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
+ });
+
+ test('parent index', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.getDiff(
+ 123 as NumericChangeId,
+ -3 as PatchSetNum,
+ 5 as PatchSetNum,
+ 'foo/bar.baz'
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+ assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
+ });
+ });
+
+ test('getDashboard', () => {
+ const fetchCacheURLStub = sinon.stub(
+ element._restApiHelper,
+ 'fetchCacheURL'
+ );
+ element.getDashboard(
+ 'gerrit/project' as RepoName,
+ 'default:main' as DashboardId
+ );
+ assert.isTrue(fetchCacheURLStub.calledOnce);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/gerrit%2Fproject/dashboards/default%3Amain'
+ );
+ });
+
+ test('getFileContent', async () => {
+ sinon.stub(element, '_getChangeURLAndSend').resolves(
+ new Response(undefined, {
+ status: 200,
+ headers: {
+ 'X-FYI-Content-Type': 'text/java',
+ },
+ }) as unknown as ParsedJSON
+ );
+
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves('new content' as unknown as ParsedJSON);
+
+ const edit = await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ 'EDIT' as PatchSetNum
+ );
+
+ assert.deepEqual(edit, {
+ content: 'new content',
+ type: 'text/java',
+ ok: true,
+ });
+
+ const normal = await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ '3' as PatchSetNum
+ );
+ assert.deepEqual(normal, {
+ content: 'new content',
+ type: 'text/java',
+ ok: true,
+ });
+ });
+
+ test('getFileContent suppresses 404s', async () => {
+ const res404 = new Response(undefined, {status: 404});
+ const res500 = new Response(undefined, {status: 500});
+ const spy = sinon.spy();
+ addListenerForTest(document, 'server-error', spy);
+ const authStub = sinon.stub(authService, 'fetch').resolves(res404);
+ sinon.stub(element, '_changeBaseURL').resolves('');
+ await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ 1 as PatchSetNum
+ );
+ await waitEventLoop();
+ assert.isFalse(spy.called);
+ authStub.reset();
+ authStub.resolves(res500);
+ await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ 1 as PatchSetNum
+ );
+ assert.isTrue(spy.called);
+ assert.notEqual(spy.lastCall.args[0].detail.response.status, 404);
+ });
+
+ test('getChangeFilesOrEditFiles is edit-sensitive', async () => {
+ const getChangeFilesStub = sinon
+ .stub(element, 'getChangeFiles')
+ .resolves({});
+ const getChangeEditFilesStub = sinon
+ .stub(element, 'getChangeEditFiles')
+ .resolves({files: {}});
+
+ await element.getChangeOrEditFiles(1 as NumericChangeId, {
+ basePatchNum: PARENT,
+ patchNum: EDIT,
+ });
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isFalse(getChangeFilesStub.called);
+ await element.getChangeOrEditFiles(1 as NumericChangeId, {
+ basePatchNum: PARENT,
+ patchNum: 1 as RevisionPatchSetNum,
+ });
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isTrue(getChangeFilesStub.calledOnce);
+ });
+
+ test('_fetch forwards request and logs', async () => {
+ const logStub = sinon.stub(element._restApiHelper, '_logCall');
+ const response = new Response(undefined, {status: 404});
+ const url = 'my url';
+ const fetchOptions = {method: 'DELETE'};
+ sinon.stub(authService, 'fetch').resolves(response);
+ const startTime = 123;
+ sinon.stub(Date, 'now').returns(startTime);
+ const req = {url, fetchOptions};
+ await element._restApiHelper.fetch(req);
+ assert.isTrue(logStub.calledOnce);
+ assert.isTrue(logStub.calledWith(req, startTime, response.status));
+ });
+
+ test('_logCall only reports requests with anonymized URLss', async () => {
+ sinon.stub(Date, 'now').returns(200);
+ const handler = sinon.stub();
+ addListenerForTest(document, 'gr-rpc-log', handler);
+
+ element._restApiHelper._logCall({url: 'url'}, 100, 200);
+ assert.isFalse(handler.called);
+
+ element._restApiHelper._logCall(
+ {url: 'url', anonymizedUrl: 'not url'},
+ 100,
+ 200
+ );
+ await waitEventLoop();
+ assert.isTrue(handler.calledOnce);
+ });
+
+ test('ported comment errors do not trigger error dialog', () => {
+ const change = createChange();
+ const handler = sinon.stub();
+ addListenerForTest(document, 'server-error', handler);
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
+ ok: false,
+ } as unknown as ParsedJSON);
+
+ element.getPortedComments(change._number, CURRENT);
+
+ assert.isFalse(handler.called);
+ });
+
+ test('ported drafts are not requested user is not logged in', () => {
+ const change = createChange();
+ sinon.stub(element, 'getLoggedIn').resolves(false);
+ const getChangeURLAndFetchStub = sinon.stub(
+ element,
+ '_getChangeURLAndFetch'
+ );
+
+ element.getPortedDrafts(change._number, CURRENT);
+
+ assert.isFalse(getChangeURLAndFetchStub.called);
+ });
+
+ test('saveChangeStarred', async () => {
+ sinon.stub(element, 'getFromProjectLookup').resolves('test' as RepoName);
+ const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+
+ await element.saveChangeStarred(123 as NumericChangeId, true);
+ assert.isTrue(sendStub.calledOnce);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: HttpMethod.PUT,
+ url: '/accounts/self/starred.changes/test~123',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+
+ await element.saveChangeStarred(456 as NumericChangeId, false);
+ assert.isTrue(sendStub.calledTwice);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: HttpMethod.DELETE,
+ url: '/accounts/self/starred.changes/test~456',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index f0a7377..984efa9 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -70,7 +70,7 @@
PreferencesInfo,
PreferencesInput,
ProjectAccessInfo,
- ProjectAccessInfoMap,
+ RepoAccessInfoMap,
ProjectAccessInput,
ProjectInfo,
ProjectInfoWithName,
@@ -287,7 +287,7 @@
errFn?: ErrorCallback
): Promise<DashboardInfo[] | undefined>;
- getRepoAccess(repo: RepoName): Promise<ProjectAccessInfoMap | undefined>;
+ getRepoAccess(repo: RepoName): Promise<RepoAccessInfoMap | undefined>;
getProjectConfig(
repo: RepoName,
@@ -370,7 +370,7 @@
): Promise<string>;
createChange(
- project: RepoName,
+ repo: RepoName,
branch: BranchName,
subject: string,
topic?: string,
@@ -513,7 +513,7 @@
deleteWatchedProjects(projects: ProjectWatchInfo[]): Promise<Response>;
- getSuggestedProjects(
+ getSuggestedRepos(
inputVal: string,
n?: number
): Promise<NameToProjectInfoMap | undefined>;
@@ -640,7 +640,7 @@
): Promise<ChangeInfo[] | undefined>;
getChangeCherryPicks(
- project: RepoName,
+ repo: RepoName,
changeID: ChangeId,
changeNum: NumericChangeId
): Promise<ChangeInfo[] | undefined>;
@@ -764,7 +764,7 @@
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard
*/
getDashboard(
- project: RepoName,
+ repo: RepoName,
dashboard: DashboardId,
errFn?: ErrorCallback
): Promise<DashboardInfo | undefined>;
@@ -802,7 +802,7 @@
getTopMenus(): Promise<TopMenuEntryInfo[] | undefined>;
- setInProjectLookup(changeNum: NumericChangeId, project: RepoName): void;
+ setInProjectLookup(changeNum: NumericChangeId, repo: RepoName): void;
getMergeable(changeNum: NumericChangeId): Promise<MergeableInfo | undefined>;
putChangeCommitMessage(
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
similarity index 96%
rename from polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
rename to polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
index 0bb02d8..3b13dbc 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
+++ b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
@@ -7,7 +7,7 @@
getAccountDisplayName,
getGroupDisplayName,
} from '../../utils/display-name-util';
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {RestApiService} from '../gr-rest-api/gr-rest-api';
import {
AccountInfo,
isReviewerAccountSuggestion,
@@ -20,7 +20,7 @@
import {assertNever} from '../../utils/common-util';
import {AutocompleteSuggestion} from '../../elements/shared/gr-autocomplete/gr-autocomplete';
import {allSettled, isFulfilled} from '../../utils/async-util';
-import {notUndefined, ParsedChangeInfo} from '../../types/types';
+import {isDefined, ParsedChangeInfo} from '../../types/types';
import {accountKey} from '../../utils/account-util';
import {
AccountId,
@@ -63,7 +63,7 @@
const suggestionsByChangeIndex = resultsByChangeIndex
.filter(isFulfilled)
.map(result => result.value)
- .filter(notUndefined);
+ .filter(isDefined);
if (suggestionsByChangeIndex.length !== resultsByChangeIndex.length) {
// one of the requests failed, so don't allow any suggestions.
return [];
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
similarity index 98%
rename from polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
rename to polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
index 15f3d24..e96a2ad 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
+++ b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
@@ -5,7 +5,7 @@
*/
import '../../test/common-test-setup';
import {GrReviewerSuggestionsProvider} from './gr-reviewer-suggestions-provider';
-import {getAppContext} from '../../services/app-context';
+import {getAppContext} from '../app-context';
import {stubRestApi} from '../../test/test-utils';
import {
AccountDetailInfo,
diff --git a/polygerrit-ui/app/services/highlight/highlight-service.ts b/polygerrit-ui/app/services/highlight/highlight-service.ts
index bfaa263..d10d875 100644
--- a/polygerrit-ui/app/services/highlight/highlight-service.ts
+++ b/polygerrit-ui/app/services/highlight/highlight-service.ts
@@ -3,6 +3,7 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {define} from '../../models/dependency';
import {
SyntaxWorkerRequest,
SyntaxWorkerInit,
@@ -39,6 +40,8 @@
*/
const CODE_MAX_LENGTH = 25 * CODE_MAX_LINES;
+export const highlightServiceToken =
+ define<HighlightService>('highlight-service');
/**
* Service for syntax highlighting. Maintains some HighlightJS workers doing
* their job in the background.
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index f8bc778..edde7a4 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable} from 'rxjs';
-import {Finalizable} from '../registry';
import {
NumericChangeId,
RevisionPatchSetNum,
@@ -12,6 +11,7 @@
} from '../../types/common';
import {Model} from '../../models/model';
import {select} from '../../utils/observable-util';
+import {define} from '../../models/dependency';
export enum GerritView {
ADMIN = 'admin',
@@ -36,7 +36,8 @@
basePatchNum?: BasePatchSetNum;
}
-export class RouterModel extends Model<RouterState> implements Finalizable {
+export const routerModelToken = define<RouterModel>('router-model');
+export class RouterModel extends Model<RouterState> {
readonly routerView$: Observable<GerritView | undefined> = select(
this.state$,
state => state.view
diff --git a/polygerrit-ui/app/services/service-worker-installer.ts b/polygerrit-ui/app/services/service-worker-installer.ts
index 53cd325..ffc5be2 100644
--- a/polygerrit-ui/app/services/service-worker-installer.ts
+++ b/polygerrit-ui/app/services/service-worker-installer.ts
@@ -12,11 +12,14 @@
import {UserModel} from '../models/user/user-model';
import {AccountDetailInfo} from '../api/rest-api';
import {until} from '../utils/async-util';
+import {LifeCycle} from '../constants/reporting';
+import {ReportingService} from './gr-reporting/gr-reporting';
/** Type of incoming messages for ServiceWorker. */
export enum ServiceWorkerMessageType {
TRIGGER_NOTIFICATIONS = 'TRIGGER_NOTIFICATIONS',
USER_PREFERENCE_CHANGE = 'USER_PREFERENCE_CHANGE',
+ REPORTING = 'REPORTING',
}
export const TRIGGER_NOTIFICATION_UPDATES_MS = 5 * 60 * 1000;
@@ -30,6 +33,7 @@
constructor(
private readonly flagsService: FlagsService,
+ private readonly reportingService: ReportingService,
private readonly userModel: UserModel
) {
if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) {
@@ -74,8 +78,19 @@
}
await registerServiceWorker('/service-worker.js');
const permission = await Notification.requestPermission();
+ this.reportingService.reportLifeCycle(LifeCycle.NOTIFICATION_PERMISSION, {
+ permission,
+ });
if (this.isPermitted(permission)) this.startTriggerTimer();
this.initialized = true;
+ // Assumption: service worker will send event only to 1 client.
+ navigator.serviceWorker.onmessage = event => {
+ if (event.data?.type === ServiceWorkerMessageType.REPORTING) {
+ this.reportingService.reportLifeCycle(LifeCycle.SERVICE_WORKER_UPDATE, {
+ eventName: event.data.eventName as string | undefined,
+ });
+ }
+ };
}
areNotificationsEnabled() {
diff --git a/polygerrit-ui/app/services/service-worker-installer_test.ts b/polygerrit-ui/app/services/service-worker-installer_test.ts
index e8fd233..a036289 100644
--- a/polygerrit-ui/app/services/service-worker-installer_test.ts
+++ b/polygerrit-ui/app/services/service-worker-installer_test.ts
@@ -9,14 +9,17 @@
import {assert} from '@open-wc/testing';
import {createDefaultPreferences} from '../constants/constants';
import {waitUntilObserved} from '../test/test-utils';
+import {testResolver} from '../test/common-test-setup';
+import {userModelToken} from '../models/user/user-model';
suite('service worker installer tests', () => {
test('init', async () => {
const registerStub = sinon.stub(window.navigator.serviceWorker, 'register');
const flagsService = getAppContext().flagsService;
- const userModel = getAppContext().userModel;
+ const reportingService = getAppContext().reportingService;
+ const userModel = testResolver(userModelToken);
sinon.stub(flagsService, 'isEnabled').returns(true);
- new ServiceWorkerInstaller(flagsService, userModel);
+ new ServiceWorkerInstaller(flagsService, reportingService, userModel);
const prefs = {
...createDefaultPreferences(),
allow_browser_notifications: true,
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index 5b38a8a..0a5e0a4 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -15,6 +15,8 @@
import {getAppContext} from '../app-context';
import {pressKey} from '../../test/test-utils';
import {assert} from '@open-wc/testing';
+import {testResolver} from '../../test/common-test-setup';
+import {userModelToken} from '../../models/user/user-model';
const KEY_A: Binding = {key: 'a'};
@@ -23,7 +25,7 @@
setup(() => {
service = new ShortcutsService(
- getAppContext().userModel,
+ testResolver(userModelToken),
getAppContext().reportingService
);
});
diff --git a/polygerrit-ui/app/services/storage/gr-storage_impl.ts b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
index 0caffbc..c177ddc 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_impl.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
@@ -6,6 +6,7 @@
import {StorageLocation, StorageObject, StorageService} from './gr-storage';
import {Finalizable} from '../registry';
import {NumericChangeId} from '../../types/common';
+import {define} from '../../models/dependency';
export const DURATION_DAY = 24 * 60 * 60 * 1000;
@@ -16,6 +17,8 @@
CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY);
CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY);
+export const storageServiceToken = define<StorageService>('storage-service');
+
export class GrStorageService implements StorageService, Finalizable {
private lastCleanup = 0;
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.ts b/polygerrit-ui/app/styles/dashboard-header-styles.ts
index cd45f8b..d9edb99 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.ts
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.ts
@@ -30,8 +30,7 @@
.info > div > span {
display: inline-block;
font-weight: var(--font-weight-bold);
- text-align: right;
- width: 4em;
+ width: 3.5em;
}
`;
diff --git a/polygerrit-ui/app/styles/gr-modal-styles.ts b/polygerrit-ui/app/styles/gr-modal-styles.ts
new file mode 100644
index 0000000..b1bcf51
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-modal-styles.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {css} from 'lit';
+
+export const modalStyles = css`
+ dialog {
+ padding: 0;
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ background: var(--dialog-background-color);
+ box-shadow: var(--elevation-level-5);
+ /*
+ * These styles are taken from main.css
+ * Dialog exists in the top-layer outside the body hence the styles
+ * in main.css were not being applied.
+ */
+ font-family: var(--font-family, ''), 'Roboto', Arial, sans-serif;
+ font-size: var(--font-size-normal, 1rem);
+ line-height: var(--line-height-normal, 1.4);
+ color: var(--primary-text-color, black);
+ }
+
+ dialog::backdrop {
+ background-color: black;
+ opacity: var(--modal-opacity, 0.6);
+ }
+`;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index e148da9..f6bab65 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -113,6 +113,8 @@
--white-10: #ffffff1a;
--white-12: #ffffff1f;
+ --modal-opacity: 0.32;
+
--error-foreground: var(--red-700);
--error-background: var(--red-50);
--error-background-hover: linear-gradient(
@@ -474,7 +476,6 @@
/* misc */
--border-radius: 4px;
- --reply-overlay-z-index: 1000;
--line-length-indicator-color: #681da8;
/* paper and iron component overrides */
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 306747b..33d2753 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -6,15 +6,13 @@
// TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
// https://github.com/Polymer/polymer-resin/issues/9 is resolved.
import '../scripts/bundled-polymer';
-import {AppContext, injectAppContext} from '../services/app-context';
+import {AppContext} from '../services/app-context';
import {Finalizable} from '../services/registry';
import {
createTestAppContext,
createTestDependencies,
- Creator,
} from './test-app-context-init';
-import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnlyResetGrRestApiSharedObjects} from '../services/gr-rest-api/gr-rest-api-impl';
+import {testOnlyResetGrRestApiSharedObjects} from '../services/gr-rest-api/gr-rest-api-impl';
import {
cleanupTestUtils,
getCleanupsCount,
@@ -23,7 +21,10 @@
removeThemeStyles,
} from './test-utils';
import {safeTypesBridge} from '../utils/safe-types-util';
-import {initGlobalVariables} from '../elements/gr-app-global-var-init';
+import {
+ initGerrit,
+ initGlobalVariables,
+} from '../elements/gr-app-global-var-init';
import {assert, fixtureCleanup} from '@open-wc/testing';
import {
_testOnly_defaultResinReportHandler,
@@ -39,6 +40,8 @@
} from '../models/dependency';
import * as sinon from 'sinon';
import '../styles/themes/app-theme.ts';
+import {Creator} from '../services/app-context-init';
+import {pluginLoaderToken} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
declare global {
interface Window {
@@ -91,7 +94,7 @@
}
function resolveDependency(evt: DependencyRequestEvent<unknown>) {
- evt.callback(testResolver(evt.dependency));
+ evt.callback(() => testResolver(evt.dependency));
}
setup(() => {
@@ -102,33 +105,35 @@
// overwritten by some other code.
assert.equal(getCleanupsCount(), 0);
appContext = createTestAppContext();
- injectAppContext(appContext);
+ initGlobalVariables(appContext);
+
finalizers.push(appContext);
const dependencies = createTestDependencies(appContext, testResolver);
for (const [token, provider] of dependencies) {
injectDependency(token, provider);
}
document.addEventListener('request-dependency', resolveDependency);
+ initGerrit(testResolver(pluginLoaderToken));
+
// The following calls is necessary to avoid influence of previously executed
// tests.
- initGlobalVariables(appContext);
-
const selection = document.getSelection();
if (selection) {
selection.removeAllRanges();
}
- const pl = _testOnly_resetPluginLoader();
// For testing, always init with empty plugin list
// Since when serve in gr-app, we always retrieve the list
// from project config and init loading after that, all
// `awaitPluginsLoaded` will rely on that to kick off,
// in testing, we want to kick start this earlier.
- // You still can manually call _testOnly_resetPluginLoader
- // to reset this behavior if you need to test something specific.
- pl.loadPlugins([]);
- _testOnlyResetGrRestApiSharedObjects();
+ testResolver(pluginLoaderToken).loadPlugins([]);
+ testOnlyResetGrRestApiSharedObjects(appContext.authService);
});
+export function removeRequestDependencyListener() {
+ document.removeEventListener('request-dependency', resolveDependency);
+}
+
// Very simple function to catch unexpected elements in documents body.
// It can't catch everything, but in most cases it is enough.
function checkChildAllowed(element: Element) {
@@ -136,23 +141,6 @@
if (allowedTags.includes(element.tagName)) {
return;
}
- if (element.tagName === 'TEST-FIXTURE') {
- if (
- element.children.length === 0 ||
- (element.children.length === 1 &&
- element.children[0].tagName === 'TEMPLATE')
- ) {
- return;
- }
- assert.fail(
- `Test fixture
- ${element.outerHTML}` +
- "isn't resotred after the test is finished. Please ensure that " +
- 'restore() method is called for this test-fixture. Usually the call' +
- 'happens automatically.'
- );
- return;
- }
if (
element.tagName === 'DIV' &&
element.id === 'gr-hovercard-container' &&
@@ -189,7 +177,7 @@
removeThemeStyles();
cancelAllTasks();
cleanUpStorage();
- document.removeEventListener('request-dependency', resolveDependency);
+ removeRequestDependencyListener();
injectedDependencies.clear();
// Reset state
for (const f of finalizers) {
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 492bd8d..a5bc4bf 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -30,7 +30,7 @@
ConfigInfo,
EditInfo,
DashboardInfo,
- ProjectAccessInfoMap,
+ RepoAccessInfoMap,
IncludedInInfo,
CommentInfo,
PathToCommentsInfoMap,
@@ -376,7 +376,7 @@
name: repo,
});
},
- getRepoAccess(): Promise<ProjectAccessInfoMap | undefined> {
+ getRepoAccess(): Promise<RepoAccessInfoMap | undefined> {
return Promise.resolve({});
},
getRepoAccessRights(): Promise<ProjectAccessInfo | undefined> {
@@ -412,7 +412,7 @@
getSuggestedGroups(): Promise<GroupNameToGroupInfoMap | undefined> {
return Promise.resolve({});
},
- getSuggestedProjects(): Promise<NameToProjectInfoMap | undefined> {
+ getSuggestedRepos(): Promise<NameToProjectInfoMap | undefined> {
return Promise.resolve({});
},
getTopMenus(): Promise<TopMenuEntryInfo[] | undefined> {
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 2cdd4a5..6bee4a6 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -6,162 +6,36 @@
// Init app context before any other imports
import {create, Registry, Finalizable} from '../services/registry';
-import {DependencyToken} from '../models/dependency';
-import {assertIsDefined} from '../utils/common-util';
import {AppContext} from '../services/app-context';
import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
import {grRestApiMock} from './mocks/gr-rest-api_mock';
import {grStorageMock} from '../services/storage/gr-storage_mock';
import {GrAuthMock} from '../services/gr-auth/gr-auth_mock';
import {FlagsServiceImplementation} from '../services/flags/flags_impl';
-import {EventEmitter} from '../services/gr-event-interface/gr-event-interface_impl';
-import {ChangeModel, changeModelToken} from '../models/change/change-model';
-import {FilesModel, filesModelToken} from '../models/change/files-model';
-import {ChecksModel, checksModelToken} from '../models/checks/checks-model';
-import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
-import {UserModel} from '../models/user/user-model';
-import {
- CommentsModel,
- commentsModelToken,
-} from '../models/comments/comments-model';
-import {RouterModel} from '../services/router/router-model';
-import {
- ShortcutsService,
- shortcutsServiceToken,
-} from '../services/shortcuts/shortcuts-service';
-import {ConfigModel, configModelToken} from '../models/config/config-model';
-import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
-import {PluginsModel} from '../models/plugins/plugins-model';
import {MockHighlightService} from '../services/highlight/highlight-service-mock';
-import {
- AccountsModel,
- accountsModelToken,
-} from '../models/accounts-model/accounts-model';
-import {
- DashboardViewModel,
- dashboardViewModelToken,
-} from '../models/views/dashboard';
-import {
- SettingsViewModel,
- settingsViewModelToken,
-} from '../models/views/settings';
-import {GrRouter, routerToken} from '../elements/core/gr-router/gr-router';
-import {AdminViewModel, adminViewModelToken} from '../models/views/admin';
-import {
- AgreementViewModel,
- 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';
-import {SearchViewModel, searchViewModelToken} from '../models/views/search';
+import {createAppDependencies, Creator} from '../services/app-context-init';
import {navigationToken} from '../elements/core/gr-navigation/gr-navigation';
+import {DependencyToken} from '../models/dependency';
+import {storageServiceToken} from '../services/storage/gr-storage_impl';
+import {highlightServiceToken} from '../services/highlight/highlight-service';
export function createTestAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
- routerModel: (_ctx: Partial<AppContext>) => new RouterModel(),
flagsService: (_ctx: Partial<AppContext>) =>
new FlagsServiceImplementation(),
reportingService: (_ctx: Partial<AppContext>) => grReportingMock,
- eventEmitter: (_ctx: Partial<AppContext>) => new EventEmitter(),
- authService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.eventEmitter, 'eventEmitter');
- return new GrAuthMock(ctx.eventEmitter);
- },
+ authService: (_ctx: Partial<AppContext>) => new GrAuthMock(),
restApiService: (_ctx: Partial<AppContext>) => grRestApiMock,
- jsApiService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new GrJsApiInterface(ctx.reportingService);
- },
- storageService: (_ctx: Partial<AppContext>) => grStorageMock,
- userModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new UserModel(ctx.restApiService);
- },
- accountsModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new AccountsModel(ctx.restApiService);
- },
- shortcutsService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.userModel, 'userModel');
- assertIsDefined(ctx.flagsService, 'flagsService');
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new ShortcutsService(ctx.userModel, ctx.reportingService);
- },
- pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
- highlightService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new MockHighlightService(ctx.reportingService);
- },
};
return create<AppContext>(appRegistry);
}
-export type Creator<T> = () => T & Finalizable;
-
-// Test dependencies are provides as creator functions to ensure that they are
-// not created if a test doesn't depend on them. E.g. don't create a
-// change-model in change-model_test.ts because it creates one in the test
-// after setting up stubs.
export function createTestDependencies(
appContext: AppContext,
resolver: <T>(token: DependencyToken<T>) => T
): Map<DependencyToken<unknown>, Creator<unknown>> {
- const dependencies = new Map<DependencyToken<unknown>, Creator<unknown>>();
- const browserModel = () => new BrowserModel(appContext.userModel);
- dependencies.set(browserModelToken, browserModel);
-
- const adminViewModelCreator = () => new AdminViewModel();
- dependencies.set(adminViewModelToken, adminViewModelCreator);
- const agreementViewModelCreator = () => new AgreementViewModel();
- dependencies.set(agreementViewModelToken, agreementViewModelCreator);
- const changeViewModelCreator = () => new ChangeViewModel();
- dependencies.set(changeViewModelToken, changeViewModelCreator);
- const dashboardViewModelCreator = () => new DashboardViewModel();
- dependencies.set(dashboardViewModelToken, dashboardViewModelCreator);
- const diffViewModelCreator = () => new DiffViewModel();
- dependencies.set(diffViewModelToken, diffViewModelCreator);
- const documentationViewModelCreator = () => new DocumentationViewModel();
- dependencies.set(documentationViewModelToken, documentationViewModelCreator);
- const editViewModelCreator = () => new EditViewModel();
- dependencies.set(editViewModelToken, editViewModelCreator);
- const groupViewModelCreator = () => new GroupViewModel();
- dependencies.set(groupViewModelToken, groupViewModelCreator);
- const pluginViewModelCreator = () => new PluginViewModel();
- dependencies.set(pluginViewModelToken, pluginViewModelCreator);
- const repoViewModelCreator = () => new RepoViewModel();
- dependencies.set(repoViewModelToken, repoViewModelCreator);
- const searchViewModelCreator = () => new SearchViewModel();
- dependencies.set(searchViewModelToken, searchViewModelCreator);
- const settingsViewModelCreator = () => new SettingsViewModel();
- dependencies.set(settingsViewModelToken, settingsViewModelCreator);
-
- const routerCreator = () =>
- new GrRouter(
- appContext.reportingService,
- appContext.routerModel,
- appContext.restApiService,
- resolver(adminViewModelToken),
- resolver(agreementViewModelToken),
- resolver(changeViewModelToken),
- resolver(dashboardViewModelToken),
- resolver(diffViewModelToken),
- resolver(documentationViewModelToken),
- resolver(editViewModelToken),
- resolver(groupViewModelToken),
- resolver(pluginViewModelToken),
- resolver(repoViewModelToken),
- resolver(searchViewModelToken),
- resolver(settingsViewModelToken)
- );
- dependencies.set(routerToken, routerCreator);
+ const dependencies = createAppDependencies(appContext, resolver);
+ dependencies.set(storageServiceToken, () => grStorageMock);
dependencies.set(navigationToken, () => {
return {
setUrl: () => {},
@@ -169,55 +43,9 @@
finalize: () => {},
};
});
-
- const changeModelCreator = () =>
- new ChangeModel(
- appContext.routerModel,
- appContext.restApiService,
- appContext.userModel
- );
- dependencies.set(changeModelToken, changeModelCreator);
-
- const accountsModelCreator = () =>
- new AccountsModel(appContext.restApiService);
- dependencies.set(accountsModelToken, accountsModelCreator);
-
- const commentsModelCreator = () =>
- new CommentsModel(
- appContext.routerModel,
- resolver(changeModelToken),
- resolver(accountsModelToken),
- appContext.restApiService,
- appContext.reportingService
- );
- dependencies.set(commentsModelToken, commentsModelCreator);
-
- const filesModelCreator = () =>
- new FilesModel(
- resolver(changeModelToken),
- resolver(commentsModelToken),
- appContext.restApiService
- );
- dependencies.set(filesModelToken, filesModelCreator);
-
- const configModelCreator = () =>
- new ConfigModel(resolver(changeModelToken), appContext.restApiService);
- dependencies.set(configModelToken, configModelCreator);
-
- const checksModelCreator = () =>
- new ChecksModel(
- appContext.routerModel,
- resolver(changeViewModelToken),
- resolver(changeModelToken),
- appContext.reportingService,
- appContext.pluginsModel
- );
-
- dependencies.set(checksModelToken, checksModelCreator);
-
- const shortcutServiceCreator = () =>
- new ShortcutsService(appContext.userModel, appContext.reportingService);
- dependencies.set(shortcutsServiceToken, shortcutServiceCreator);
-
+ dependencies.set(
+ highlightServiceToken,
+ () => new MockHighlightService(appContext.reportingService)
+ );
return dependencies;
}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 4e29f53..08ac65f 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -388,6 +388,7 @@
messages.push({
...createChangeMessageInfo((i + messageIdStart).toString(16)),
date: dateToTimestamp(messageDate),
+ author: createAccountDetailWithId(i),
});
messageDate = new Date(messageDate);
messageDate.setDate(messageDate.getDate() + 1);
@@ -559,8 +560,7 @@
content: [
{
ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
- 'nulla phasellus.',
+ 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula.',
'Mattis lectus.',
'Sodales duis.',
'Orci a faucibus.',
@@ -631,7 +631,7 @@
'Etiam dui, blandit wisi.',
'Mi nec.',
'Vitae eget vestibulum.',
- 'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
+ 'Ullamcorper nunc ante, nec imperdiet felis, consectetur.',
'Ac eget.',
'Vel fringilla, interdum pellentesque placerat, proin ante.',
],
@@ -698,7 +698,7 @@
return {
view: GerritView.CHANGE,
changeNum: TEST_NUMERIC_CHANGE_ID,
- project: TEST_PROJECT_NAME,
+ repo: TEST_PROJECT_NAME,
};
}
@@ -707,6 +707,8 @@
view: GerritView.SEARCH,
query: TEST_NUMERIC_CHANGE_ID.toString(),
offset: '0',
+ changes: [],
+ loading: false,
};
}
@@ -716,7 +718,7 @@
changeNum: TEST_NUMERIC_CHANGE_ID,
patchNum: EDIT,
path: 'foo/bar.baz',
- project: TEST_PROJECT_NAME,
+ repo: TEST_PROJECT_NAME,
};
}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index d6ad434..6ab0924 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -4,21 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../types/globals';
-import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
import {getAppContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {SinonSpy, SinonStub} from 'sinon';
-import {StorageService} from '../services/storage/gr-storage';
-import {AuthService} from '../services/gr-auth/gr-auth';
import {ReportingService} from '../services/gr-reporting/gr-reporting';
-import {UserModel} from '../models/user/user-model';
import {queryAndAssert, query} from '../utils/common-util';
import {FlagsService} from '../services/flags/flags';
-import {Key, Modifier} from '../utils/dom-util';
+import {Key, Modifier, whenVisible} from '../utils/dom-util';
import {Observable} from 'rxjs';
import {filter, take, timeout} from 'rxjs/operators';
-import {HighlightService} from '../services/highlight/highlight-service';
import {assert} from '@open-wc/testing';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
@@ -51,14 +45,6 @@
return getComputedStyle(el).getPropertyValue('display') !== 'none';
}
-// Provide reset plugins function to clear installed plugins between tests.
-// No gr-app found (running tests)
-export const resetPlugins = () => {
- _testOnly_resetEndpoints();
- const pl = _testOnly_resetPluginLoader();
- pl.loadPlugins([]);
-};
-
export type CleanupCallback = () => void;
const cleanups: CleanupCallback[] = [];
@@ -109,28 +95,6 @@
return sinon.spy(getAppContext().restApiService, method);
}
-export function stubUsers<K extends keyof UserModel>(method: K) {
- return sinon.stub(getAppContext().userModel, method);
-}
-
-export function stubHighlightService<K extends keyof HighlightService>(
- method: K
-) {
- return sinon.stub(getAppContext().highlightService, method);
-}
-
-export function stubStorage<K extends keyof StorageService>(method: K) {
- return sinon.stub(getAppContext().storageService, method);
-}
-
-export function spyStorage<K extends keyof StorageService>(method: K) {
- return sinon.spy(getAppContext().storageService, method);
-}
-
-export function stubAuth<K extends keyof AuthService>(method: K) {
- return sinon.stub(getAppContext().authService, method);
-}
-
export function stubReporting<K extends keyof ReportingService>(method: K) {
return sinon.stub(getAppContext().reportingService, method);
}
@@ -220,6 +184,12 @@
});
}
+export async function waitUntilVisible(element: Element): Promise<void> {
+ return new Promise(resolve => {
+ whenVisible(element, () => resolve());
+ });
+}
+
export function waitUntilCalled(stub: SinonStub | SinonSpy, name: string) {
return waitUntil(() => stub.called, `${name} was not called`);
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 008a8de..93a3f2a 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -6,7 +6,7 @@
import {CommentRange} from '../api/core';
import {
ChangeStatus,
- ProjectState,
+ RepoState,
SubmitType,
InheritedBooleanInfoConfiguredValue,
PermissionAction,
@@ -769,13 +769,13 @@
can_add?: boolean;
can_add_tags?: boolean;
config_visible?: boolean;
- groups: ProjectAccessGroups;
+ groups: RepoAccessGroups;
config_web_links: WebLinkInfo[];
}
-export type ProjectAccessInfoMap = {[projectName: string]: ProjectAccessInfo};
+export type RepoAccessInfoMap = {[projectName: string]: ProjectAccessInfo};
export type LocalAccessSectionInfo = {[ref: string]: AccessSectionInfo};
-export type ProjectAccessGroups = {[uuid: string]: GroupInfo};
+export type RepoAccessGroups = {[uuid: string]: GroupInfo};
/**
* The AccessSectionInfo describes the access rights that are assigned on a ref.
@@ -858,7 +858,7 @@
reject_empty_commit?: InheritedBooleanInfoConfiguredValue;
max_object_size_limit?: MaxObjectSizeLimitInfo;
submit_type?: SubmitType;
- state?: ProjectState;
+ state?: RepoState;
plugin_config_values?: PluginNameToPluginParametersMap;
commentlinks?: ConfigInfoCommentLinks;
}
@@ -907,13 +907,13 @@
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-access-input
*/
export interface ProjectAccessInput {
- remove?: RefToProjectAccessInfoMap;
- add?: RefToProjectAccessInfoMap;
+ remove?: RefToRepoAccessInfoMap;
+ add?: RefToRepoAccessInfoMap;
message?: string;
parent?: string;
}
-export type RefToProjectAccessInfoMap = {[refName: string]: ProjectAccessInfo};
+export type RefToRepoAccessInfoMap = {[refName: string]: ProjectAccessInfo};
/**
* Represent a file in a base64 encoding
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 496513d..f642af7 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -90,6 +90,7 @@
'server-error': ServerErrorEvent;
'show-alert': ShowAlertEvent;
'show-error': ShowErrorEvent;
+ 'auth-error': AuthErrorEvent;
}
}
@@ -206,6 +207,12 @@
}
export type ShowErrorEvent = CustomEvent<ShowErrorEventDetail>;
+export interface AuthErrorEventDetail {
+ message: string;
+ action: string;
+}
+export type AuthErrorEvent = CustomEvent<AuthErrorEventDetail>;
+
// Type for the custom event to switch tab.
export interface SwitchTabEventDetail {
// name of the tab to set as active, from custom event
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 320ac66..8d55e36 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -20,8 +20,8 @@
} from './common';
import {AuthRequestInit} from '../services/gr-auth/gr-auth';
-export function notUndefined<T>(x: T | undefined): x is T {
- return x !== undefined;
+export function isDefined<T>(x: T): x is NonNullable<T> {
+ return x !== undefined && x !== null;
}
export interface FixIronA11yAnnouncer extends IronA11yAnnouncer {
diff --git a/polygerrit-ui/app/utils/admin-nav-util.ts b/polygerrit-ui/app/utils/admin-nav-util.ts
index c8fc9fb..6f81d57 100644
--- a/polygerrit-ui/app/utils/admin-nav-util.ts
+++ b/polygerrit-ui/app/utils/admin-nav-util.ts
@@ -64,11 +64,11 @@
): Promise<AdminLinks> {
if (!account) {
return Promise.resolve(
- _filterLinks(link => !!link.viewableToAll, getAdminMenuLinks, options)
+ filterLinks(link => !!link.viewableToAll, getAdminMenuLinks, options)
);
}
return getAccountCapabilities().then(capabilities =>
- _filterLinks(
+ filterLinks(
link => !link.capability || hasOwnProperty(capabilities, link.capability),
getAdminMenuLinks,
options
@@ -76,7 +76,7 @@
);
}
-function _filterLinks(
+function filterLinks(
filterFn: (link: NavLink) => boolean,
getAdminMenuLinks: () => MenuLink[],
options?: AdminNavLinksOption
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 4281f43..cae6319 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -245,3 +245,56 @@
)
);
}
+
+/**
+ * Noop function that can be used to suppress the tsetse must-use-promises rule.
+ *
+ * Example Usage:
+ * async function x() {
+ * await doA();
+ * noAwait(doB());
+ * }
+ */
+export function noAwait(_: {then: Function} | null | undefined) {}
+
+export interface CancelablePromise<T> extends Promise<T> {
+ cancel(): void;
+}
+
+/**
+ * Make the promise cancelable.
+ *
+ * Returns a promise with a `cancel()` method wrapped around `promise`.
+ * Calling `cancel()` will reject the returned promise with
+ * {isCancelled: true} synchronously. If the inner promise for a cancelled
+ * promise resolves or rejects this is ignored.
+ */
+export function makeCancelable<T>(promise: Promise<T>) {
+ // True if the promise is either resolved or reject (possibly cancelled)
+ let isDone = false;
+
+ let rejectPromise: (reason?: unknown) => void;
+
+ const wrappedPromise: CancelablePromise<T> = new Promise(
+ (resolve, reject) => {
+ rejectPromise = reject;
+ promise.then(
+ val => {
+ if (!isDone) resolve(val);
+ isDone = true;
+ },
+ error => {
+ if (!isDone) reject(error);
+ isDone = true;
+ }
+ );
+ }
+ ) as CancelablePromise<T>;
+
+ wrappedPromise.cancel = () => {
+ if (isDone) return;
+ rejectPromise({isCanceled: true});
+ isDone = true;
+ };
+ return wrappedPromise;
+}
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 9062ac7..aa54318 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -109,11 +109,11 @@
}
export function changeBaseURL(
- project: string,
+ repo: string,
changeNum: NumericChangeId,
patchNum: PatchSetNum
): string {
- let v = `${getBaseUrl()}/changes/${encodeURIComponent(project)}~${changeNum}`;
+ let v = `${getBaseUrl()}/changes/${encodeURIComponent(repo)}~${changeNum}`;
if (patchNum) {
v += `/revisions/${patchNum}`;
}
diff --git a/polygerrit-ui/app/utils/display-name-util.ts b/polygerrit-ui/app/utils/display-name-util.ts
index 7c39e1a..850509f 100644
--- a/polygerrit-ui/app/utils/display-name-util.ts
+++ b/polygerrit-ui/app/utils/display-name-util.ts
@@ -56,21 +56,21 @@
account: AccountInfo
) {
const reviewerName = getDisplayName(config, account);
- const reviewerEmail = _accountEmail(account.email);
+ const reviewerEmail = accountEmail(account.email);
const reviewerStatus = account.status ? '(' + account.status + ')' : '';
return [reviewerName, reviewerEmail, reviewerStatus]
.filter(p => p.length > 0)
.join(' ');
}
-function _accountEmail(email?: string) {
+function accountEmail(email?: string) {
if (typeof email !== 'undefined') {
return '<' + email + '>';
}
return '';
}
-export const _testOnly_accountEmail = _accountEmail;
+export const _testOnly_accountEmail = accountEmail;
export function getGroupDisplayName(group: GroupInfo) {
return `${group.name || ''} (group)`;
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 0b53f61..23e3356 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -182,25 +182,37 @@
}
/**
- * Are any ancestors of the element (or the element itself) members of the
- * given class.
+ * Are any ancestors of the element (or the element itself) tagged with the
+ * given css class?
*
+ * We are walking up the DOM using `element.parentElement`, but are not crossing
+ * Shadow DOM boundaries, if there are any.
*/
export function descendedFromClass(
- element: Element,
+ element: Element | undefined,
className: string,
stopElement?: Element
) {
- let isDescendant = element.classList.contains(className);
- while (
- !isDescendant &&
- element.parentElement &&
- (!stopElement || element.parentElement !== stopElement)
- ) {
- isDescendant = element.classList.contains(className);
- element = element.parentElement;
+ return parentWithClass(element, className, stopElement) !== undefined;
+}
+
+/**
+ * Returns an ancestor of the element (or the element itself) tagged with the
+ * given css class - or undefined.
+ *
+ * We are walking up the DOM using `element.parentElement`, but are not crossing
+ * Shadow DOM boundaries, if there are any.
+ */
+export function parentWithClass(
+ element: Element | undefined,
+ className: string,
+ stopElement?: Element
+) {
+ while (element && (!stopElement || element !== stopElement)) {
+ if (element.classList.contains(className)) return element;
+ element = element.parentElement ?? undefined;
}
- return isDescendant;
+ return undefined;
}
/**
@@ -453,7 +465,7 @@
const path: EventTarget[] = e.composedPath() ?? [];
for (const el of path) {
if (!isElementTarget(el)) continue;
- if (el.tagName === 'GR-OVERLAY') return true;
+ if (el.tagName === 'GR-OVERLAY' || el.tagName === 'DIALOG') return true;
}
return false;
}
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 4b52548..3ad4438 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -59,11 +59,13 @@
}
async function createFixture() {
- return await fixture<HTMLElement>(html` <div id="test" class="a b c">
- <a class="testBtn" style="color:red;"></a>
- <dom-util-test-element></dom-util-test-element>
- <span class="ss"></span>
- </div>`);
+ return await fixture<HTMLElement>(html`
+ <div id="test" class="a b c d">
+ <a class="testBtn" style="color:red;"></a>
+ <dom-util-test-element></dom-util-test-element>
+ <span class="ss"></span>
+ </div>
+ `);
}
suite('dom-util tests', () => {
@@ -127,7 +129,7 @@
path = getEventPath(e as MouseEvent);
});
aLink.click();
- assert.equal(path, 'html>body>div>div#test.a.b.c>a.testBtn');
+ assert.equal(path, 'html>body>div>div#test.a.b.c.d>a.testBtn');
});
});
@@ -150,14 +152,44 @@
});
suite('descendedFromClass', () => {
- test('basic tests', async () => {
+ test('descends from itself', async () => {
const element = await createFixture();
const testEl = queryAndAssert(element, 'dom-util-test-element');
- // .c is a child of .a and not vice versa.
- assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.c'), 'a'));
- assert.isFalse(descendedFromClass(queryAndAssert(testEl, '.a'), 'c'));
+ assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.c'), 'c'));
+ assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.b'), 'b'));
+ assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.a'), 'a'));
+ });
- // Stops at stop element.
+ test('.c in .b in .a', async () => {
+ const element = await createFixture();
+ const testEl = queryAndAssert(element, 'dom-util-test-element');
+ const a = queryAndAssert(testEl, '.a');
+ const b = queryAndAssert(testEl, '.b');
+ const c = queryAndAssert(testEl, '.c');
+ assert.isTrue(descendedFromClass(a, 'a'));
+ assert.isTrue(descendedFromClass(b, 'a'));
+ assert.isTrue(descendedFromClass(c, 'a'));
+ assert.isFalse(descendedFromClass(a, 'b'));
+ assert.isTrue(descendedFromClass(b, 'b'));
+ assert.isTrue(descendedFromClass(c, 'b'));
+ assert.isFalse(descendedFromClass(a, 'c'));
+ assert.isFalse(descendedFromClass(b, 'c'));
+ assert.isTrue(descendedFromClass(c, 'c'));
+ });
+
+ test('stops at shadow root', async () => {
+ const element = await createFixture();
+ const testEl = queryAndAssert(element, 'dom-util-test-element');
+ const a = queryAndAssert(testEl, '.a');
+ // div.d is a parent of testEl, but `descendedFromClass` does not cross
+ // the shadow root boundary of <dom-util-test-element>. So div.a inside
+ // the shadow root is not considered to descend from div.d outside of it.
+ assert.isFalse(descendedFromClass(a, 'd'));
+ });
+
+ test('stops at stop element', async () => {
+ const element = await createFixture();
+ const testEl = queryAndAssert(element, 'dom-util-test-element');
assert.isFalse(
descendedFromClass(
queryAndAssert(testEl, '.c'),
diff --git a/polygerrit-ui/app/utils/file-util.ts b/polygerrit-ui/app/utils/file-util.ts
new file mode 100644
index 0000000..246ac20
--- /dev/null
+++ b/polygerrit-ui/app/utils/file-util.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/** See also Patch.java for the backend equivalent. */
+export enum FileMode {
+ /** Mode indicating an entry is a symbolic link. */
+ SYMLINK = 0o120000,
+
+ /** Mode indicating an entry is a non-executable file. */
+ REGULAR_FILE = 0o100644,
+
+ /** Mode indicating an entry is an executable file. */
+ EXECUTABLE_FILE = 0o100755,
+
+ /** Mode indicating an entry is a submodule commit in another repository. */
+ GITLINK = 0o160000,
+}
+
+export function fileModeToString(mode?: number, includeNumber = true): string {
+ const str = fileModeStr(mode);
+ const num = mode?.toString(8);
+ return `${str}${includeNumber && str ? ` (${num})` : ''}`;
+}
+
+function fileModeStr(mode?: number): string {
+ if (mode === FileMode.SYMLINK) return 'symlink';
+ if (mode === FileMode.REGULAR_FILE) return 'regular';
+ if (mode === FileMode.EXECUTABLE_FILE) return 'executable';
+ if (mode === FileMode.GITLINK) return 'gitlink';
+ return '';
+}
+
+export function expandFileMode(input?: string) {
+ if (!input) return input;
+ for (const modeNum of Object.values(FileMode) as FileMode[]) {
+ const modeStr = modeNum?.toString(8);
+ if (input.includes(modeStr)) {
+ return input.replace(modeStr, `${fileModeToString(modeNum)}`);
+ }
+ }
+ return input;
+}
diff --git a/polygerrit-ui/app/utils/file-util_test.ts b/polygerrit-ui/app/utils/file-util_test.ts
new file mode 100644
index 0000000..aeab026
--- /dev/null
+++ b/polygerrit-ui/app/utils/file-util_test.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {assert} from '@open-wc/testing';
+import '../test/common-test-setup';
+import {expandFileMode, FileMode, fileModeToString} from './file-util';
+
+suite('file-util tests', () => {
+ test('fileModeToString', () => {
+ const check = (
+ mode: number | undefined,
+ str: string,
+ includeNumber = true
+ ) => assert.equal(fileModeToString(mode, includeNumber), str);
+
+ check(undefined, '');
+ check(0, '');
+ check(1, '');
+ check(FileMode.REGULAR_FILE, 'regular', false);
+ check(FileMode.EXECUTABLE_FILE, 'executable', false);
+ check(FileMode.SYMLINK, 'symlink', false);
+ check(FileMode.GITLINK, 'gitlink', false);
+ check(FileMode.REGULAR_FILE, 'regular (100644)');
+ check(FileMode.EXECUTABLE_FILE, 'executable (100755)');
+ check(FileMode.SYMLINK, 'symlink (120000)');
+ check(FileMode.GITLINK, 'gitlink (160000)');
+ });
+
+ test('expandFileMode', () => {
+ assert.deepEqual(['asdf'].map(expandFileMode), ['asdf']);
+ assert.deepEqual(
+ ['old mode 100644', 'new mode 100755'].map(expandFileMode),
+ ['old mode regular (100644)', 'new mode executable (100755)']
+ );
+ });
+});
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index 515f2e7..355e54b 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -206,7 +206,7 @@
};
});
}
- return _computeWipForPatchSets(change, patchNums);
+ return computeWipForPatchSets(change, patchNums);
}
/**
@@ -218,7 +218,7 @@
* @return The given list of patch set objects, with the
* wip property set on each of them
*/
-function _computeWipForPatchSets(
+function computeWipForPatchSets(
change: ChangeInfo | ParsedChangeInfo,
patchNums: PatchSet[]
) {
@@ -249,7 +249,7 @@
return patchNums;
}
-export const _testOnly_computeWipForPatchSets = _computeWipForPatchSets;
+export const _testOnly_computeWipForPatchSets = computeWipForPatchSets;
export function computeLatestPatchNum(
allPatchSets?: PatchSet[]
diff --git a/polygerrit-ui/app/utils/string-util.ts b/polygerrit-ui/app/utils/string-util.ts
index b6f1ad1..659fc20 100644
--- a/polygerrit-ui/app/utils/string-util.ts
+++ b/polygerrit-ui/app/utils/string-util.ts
@@ -14,10 +14,6 @@
return `${count} ${noun}` + (count > 1 ? 's' : '');
}
-export function addQuotesWhen(string: string, cond: boolean): string {
- return cond ? `"${string}"` : string;
-}
-
export function charsOnly(s: string): string {
return s.replace(/[^a-zA-Z]+/g, '');
}
@@ -30,6 +26,15 @@
return `${n}th`;
}
+/** Escape operator value to avoid affecting overall query.
+ *
+ * Escapes quotes (") and backslashes (\). Wraps in quotes so the value can
+ * contain spaces and colons.
+ */
+export function escapeAndWrapSearchOperatorValue(value: string): string {
+ return `"${value.replace('\\', '\\\\').replace('"', '\\"')}"`;
+}
+
/**
* This converts any inputed value into string.
*
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 54a6838..8564c3f 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -76,7 +76,7 @@
return getDocsBaseUrlCachedPromise;
}
-export function _testOnly_clearDocsBaseUrlCache() {
+export function testOnly_clearDocsBaseUrlCache() {
getDocsBaseUrlCachedPromise = undefined;
}
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index aa80b73..a014dc2 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -13,7 +13,7 @@
import {
getBaseUrl,
getDocsBaseUrl,
- _testOnly_clearDocsBaseUrlCache,
+ testOnly_clearDocsBaseUrlCache,
encodeURL,
singleDecodeURL,
toPath,
@@ -47,7 +47,7 @@
suite('getDocsBaseUrl tests', () => {
setup(() => {
- _testOnly_clearDocsBaseUrlCache();
+ testOnly_clearDocsBaseUrlCache();
appContext = getAppContext();
});
diff --git a/polygerrit-ui/app/workers/service-worker-class.ts b/polygerrit-ui/app/workers/service-worker-class.ts
index 218744d..f9cc591 100644
--- a/polygerrit-ui/app/workers/service-worker-class.ts
+++ b/polygerrit-ui/app/workers/service-worker-class.ts
@@ -18,6 +18,7 @@
} from './service-worker-indexdb';
import {createDashboardUrl} from '../models/views/dashboard';
import {createChangeUrl} from '../models/views/change';
+import {noAwait} from '../utils/async-util';
export class ServiceWorker {
constructor(
@@ -133,6 +134,7 @@
// TODO(milutin): Add gerrit host icon
this.ctx.registration.showNotification(change.subject, {body, data});
+ this.sendReport('notify about 1 change');
}
private showNotificationForDashboard(numOfChangesToNotifyAbout: number) {
@@ -140,6 +142,7 @@
const dashboardUrl = createDashboardUrl({});
const data = {url: `${self.location.origin}${dashboardUrl}`};
this.ctx.registration.showNotification(title, {data});
+ this.sendReport(`notify about ${numOfChangesToNotifyAbout} changes`);
}
// private but used in test
@@ -154,6 +157,7 @@
const prevLatestUpdateTimestampMs = this.latestUpdateTimestampMs;
this.latestUpdateTimestampMs = Date.now();
await this.saveState();
+ noAwait(this.sendReport('polling'));
const changes = await this.getLatestAttentionSetChanges();
const latestAttentionChanges = filterAttentionChangesAfter(
changes,
@@ -173,4 +177,19 @@
const changes = payload.parsed as unknown as ParsedChangeInfo[] | undefined;
return changes ?? [];
}
+
+ /**
+ * Send report event to 1 client (last focused one). The client will use
+ * gr-reporting service to send event to metric event collectors.
+ */
+ async sendReport(eventName: string) {
+ const clientsArr = await this.ctx.clients.matchAll({type: 'window'});
+ const lastFocusedClient = clientsArr?.[0];
+ if (!lastFocusedClient) return;
+
+ lastFocusedClient.postMessage({
+ type: ServiceWorkerMessageType.REPORTING,
+ eventName,
+ });
+ }
}
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 1287d0c..6fa4d0f 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -11,6 +11,7 @@
"@open-wc/testing": "^3.1.6",
"@web/dev-server-esbuild": "^0.3.2",
"@web/test-runner": "^0.14.0",
+ "@web/test-runner-playwright": "^0.9.0",
"@web/test-runner-visual-regression": "^0.6.6",
"accessibility-developer-tools": "^2.12.0",
"karma": "^6.3.20",
@@ -25,6 +26,7 @@
"test": "web-test-runner",
"test:screenshot": "web-test-runner --run-screenshots",
"test:screenshot-update": "web-test-runner --update-screenshots --files",
+ "test:browsers": "web-test-runner --playwright --browsers webkit firefox chromium",
"test:coverage": "web-test-runner --coverage",
"test:watch": "web-test-runner --watch",
"test:single": "web-test-runner --watch --files",
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index ca6943d..35409a8 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -965,7 +965,7 @@
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
-"@jridgewell/resolve-uri@^3.0.3":
+"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
@@ -975,11 +975,19 @@
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
-"@jridgewell/sourcemap-codec@^1.4.10":
+"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+"@jridgewell/trace-mapping@^0.3.12":
+ version "0.3.17"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
+ integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+ dependencies:
+ "@jridgewell/resolve-uri" "3.1.0"
+ "@jridgewell/sourcemap-codec" "1.4.14"
+
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.15"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774"
@@ -1883,6 +1891,16 @@
picomatch "^2.2.2"
v8-to-istanbul "^8.0.0"
+"@web/test-runner-coverage-v8@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.5.0.tgz#d1b033fd4baddaf5636a41cd017e321a338727a6"
+ integrity sha512-4eZs5K4JG7zqWEhVSO8utlscjbVScV7K6JVwoWWcObFTGAaBMbDVzwGRimyNSzvmfTdIO/Arze4CeUUfCl4iLQ==
+ dependencies:
+ "@web/test-runner-core" "^0.10.20"
+ istanbul-lib-coverage "^3.0.0"
+ picomatch "^2.2.2"
+ v8-to-istanbul "^9.0.1"
+
"@web/test-runner-mocha@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@web/test-runner-mocha/-/test-runner-mocha-0.7.5.tgz#696f8cb7f5118a72bd7ac5778367ae3bd3fb92cd"
@@ -1891,6 +1909,15 @@
"@types/mocha" "^8.2.0"
"@web/test-runner-core" "^0.10.20"
+"@web/test-runner-playwright@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-playwright/-/test-runner-playwright-0.9.0.tgz#c13b71ecfe763ae5d15dff586a35a9840c238b1f"
+ integrity sha512-RhWkz1CY3KThHoX89yZ/gz9wDSPujxd2wMWNxqhov4y/XDI+0TS44TWKBfWXnuvlQFZPi8JFT7KibCo3pb/Mcg==
+ dependencies:
+ "@web/test-runner-core" "^0.10.20"
+ "@web/test-runner-coverage-v8" "^0.5.0"
+ playwright "^1.22.2"
+
"@web/test-runner-visual-regression@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@web/test-runner-visual-regression/-/test-runner-visual-regression-0.6.6.tgz#4a4dc734f360cba66a005e07b4a1c0a9ef956444"
@@ -4626,6 +4653,18 @@
dependencies:
find-up "^4.0.0"
+playwright-core@1.27.1:
+ version "1.27.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.27.1.tgz#840ef662e55a3ed759d8b5d3d00a5f885a7184f4"
+ integrity sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==
+
+playwright@^1.22.2:
+ version "1.27.1"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.27.1.tgz#4eecac5899566c589d4220ca8acc16abe8a67450"
+ integrity sha512-xXYZ7m36yTtC+oFgqH0eTgullGztKSRMb4yuwLPl8IYSmgBM88QiB+3IWb1mRIC9/NNwcgbG0RwtFlg+EAFQHQ==
+ dependencies:
+ playwright-core "1.27.1"
+
pngjs@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
@@ -5555,6 +5594,15 @@
convert-source-map "^1.6.0"
source-map "^0.7.3"
+v8-to-istanbul@^9.0.1:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4"
+ integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.12"
+ "@types/istanbul-lib-coverage" "^2.0.1"
+ convert-source-map "^1.6.0"
+
valid-url@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
diff --git a/proto/cache.proto b/proto/cache.proto
index 83c2ce2..9aadf0f 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -696,7 +696,7 @@
// Serialized form of
// com.google.gerrit.server.patch.filediff.FileDiffOutput
-// Next ID: 13
+// Next ID: 15
message FileDiffOutputProto {
// Next ID: 5
message Edit {
@@ -728,4 +728,6 @@
bytes new_commit = 10;
ComparisonType comparison_type = 11;
bool negative = 12;
+ string old_mode = 13; // ENUM as string
+ string new_mode = 14; // ENUM as string
}
diff --git a/tools/BUILD b/tools/BUILD
index 4e4e5f0..e25dcc5 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -46,11 +46,13 @@
"-XepDisableWarningsInGeneratedCode",
# The XepDisableWarningsInGeneratedCode disables only warnings, but
# not errors. We should manually exclude all files generated by
- # AutoValue; such files always start $AutoValue_.....
+ # AutoValue; such files always start AutoValue_..., $AutoValue_...,
+ # $$AutoValue_... or AutoValueGson_...
# XepExcludedPaths is a regexp. If you need more paths - use | as
# separator.
- "-XepExcludedPaths:.*/\\\\$$AutoValue_.*\\.java",
+ "-XepExcludedPaths:.*/\\\\$$?\\\\$$?AutoValue(Gson)?_.*\\.java",
"-Xep:AlmostJavadoc:ERROR",
+ "-Xep:AlreadyChecked:ERROR",
"-Xep:AlwaysThrows:ERROR",
"-Xep:AmbiguousMethodReference:ERROR",
"-Xep:AnnotateFormatMethod:ERROR",
@@ -68,7 +70,7 @@
"-Xep:AutoValueConstructorOrderChecker:ERROR",
"-Xep:AutoValueFinalMethods:ERROR",
"-Xep:AutoValueImmutableFields:ERROR",
- # "-Xep:AutoValueSubclassLeaked:WARN",
+ "-Xep:AutoValueSubclassLeaked:ERROR",
"-Xep:BadAnnotationImplementation:ERROR",
"-Xep:BadComparable:ERROR",
"-Xep:BadImport:ERROR",
@@ -124,7 +126,7 @@
"-Xep:DoNotCallSuggester:ERROR",
"-Xep:DoNotClaimAnnotations:ERROR",
"-Xep:DoNotMock:ERROR",
- "-Xep:DoNotMockAutoValue:WARN",
+ "-Xep:DoNotMockAutoValue:ERROR",
"-Xep:DoubleBraceInitialization:ERROR",
"-Xep:DoubleCheckedLocking:ERROR",
"-Xep:DuplicateMapKeys:ERROR",
@@ -145,7 +147,7 @@
"-Xep:EqualsUsingHashCode:ERROR",
"-Xep:EqualsWrongThing:ERROR",
"-Xep:ErroneousThreadPoolConstructorChecker:ERROR",
- "-Xep:EscapedEntity:WARN",
+ "-Xep:EscapedEntity:ERROR",
"-Xep:ExpectedExceptionChecker:ERROR",
"-Xep:ExtendingJUnitAssert:ERROR",
"-Xep:ExtendsAutoValue:ERROR",
@@ -240,7 +242,7 @@
"-Xep:JavaLocalTimeGetNano:ERROR",
"-Xep:JavaPeriodGetDays:ERROR",
"-Xep:JavaTimeDefaultTimeZone:ERROR",
- "-Xep:JavaUtilDate:WARN",
+ "-Xep:JavaUtilDate:ERROR",
"-Xep:JdkObsolete:ERROR",
"-Xep:JodaConstructors:ERROR",
"-Xep:JodaDateTimeConstants:ERROR",
@@ -350,6 +352,7 @@
"-Xep:RestrictedApiChecker:ERROR",
"-Xep:RethrowReflectiveOperationExceptionAsLinkageError:ERROR",
"-Xep:ReturnFromVoid:ERROR",
+ "-Xep:ReturnMissingNullable:ERROR",
"-Xep:ReturnValueIgnored:ERROR",
"-Xep:RxReturnValueIgnored:ERROR",
"-Xep:SameNameButDifferent:ERROR",
@@ -425,6 +428,7 @@
"-Xep:WrongOneof:ERROR",
"-Xep:XorPower:ERROR",
"-Xep:ZoneIdOfZ:ERROR",
+ "-Xlint:unchecked",
],
packages = ["error_prone_packages"],
)
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 37d8b9c..cf50499 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -67,18 +67,18 @@
sha1 = "cb2f351bf4463751201f43bb99865235d5ba07ca",
)
- SSHD_VERS = "2.8.0"
+ SSHD_VERS = "2.9.1"
maven_jar(
name = "sshd-osgi",
artifact = "org.apache.sshd:sshd-osgi:" + SSHD_VERS,
- sha1 = "b2a59b73c045f40d5722b9160d4f909a646d86c9",
+ sha1 = "9ed1a653da98a1aabe3ae092ee8310299718e914",
)
maven_jar(
name = "sshd-sftp",
artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
- sha1 = "d3cd9bc8d335b3ed1a86d2965deb4d202de27442",
+ sha1 = "6d01cb8138e60e97e3de08e96cc5a094c8ce2cac",
)
maven_jar(
@@ -96,7 +96,7 @@
maven_jar(
name = "sshd-mina",
artifact = "org.apache.sshd:sshd-mina:" + SSHD_VERS,
- sha1 = "02f78100cce376198be798a37c84aaf945e8a0f7",
+ sha1 = "5ab797b99630bb0c3e9ebcd8a3a6cad46408a79a",
)
maven_jar(
@@ -135,8 +135,8 @@
maven_jar(
name = "error-prone-annotations",
- artifact = "com.google.errorprone:error_prone_annotations:2.10.0",
- sha1 = "9bc20b94d3ac42489cf6ce1e42509c86f6f861a1",
+ artifact = "com.google.errorprone:error_prone_annotations:2.15.0",
+ sha1 = "38c8485a652f808c8c149150da4e5c2b0bd17f9a",
)
FLOGGER_VERS = "0.7.4"